無鎖程式碼下,在讀寫欄位時使用記憶體屏障往往是不夠的。在 64 位欄位上進行加、減操作需要使用Interlocked
工具類這樣更加重型的方式。Interlocked
也提供了Exchange
和CompareExchange
方法,後者能夠進行無鎖的讀-改-寫(read-modify-write)操作,只需要額外增加一點程式碼。
如果一條語句在底層處理器上被當作一個獨立不可分割的指令,那麼它本質上是原子的(atomic)。嚴格的原子性可以阻止任何搶佔的可能。對於 32 位(或更低)的欄位的簡單讀寫總是原子的。而操作 64 位欄位僅在 64 位執行時環境下是原子的,並且結合了多個讀寫操作的語句必然不是原子的:
class Atomicity
{
static int _x, _y;
static long _z;
static void Test()
{
long myLocal;
_x = 3; // 原子的
_z = 3; // 32位環境下不是原子的(_z 是64位的)
myLocal = _z; // 32位環境下不是原子的(_z 是64位的)
_y += _x; // 不是原子的 (結合了讀和寫操作)
_x++; // 不是原子的 (結合了讀和寫操作)
}
}
在 32 位環境下讀寫 64 位欄位不是原子的,因為它需要兩條獨立的指令:每條用於對應的 32 位記憶體地址。所以,如果執行緒 X 在讀一個 64 位的值,同時執行緒 Y 更新它,那麼執行緒 X 最終可能得到新舊兩個值按位組合後的結果(一個撕裂讀(torn read))。
編譯器實現x++
這種一元運算,是通過先讀一個變數,然後計算,最後寫回去的方式。考慮如下類:
class ThreadUnsafe
{
static int _x = 1000;
static void Go() { for (int i = 0; i < 100; i++) _x--; }
}
拋開記憶體屏障的事情,你可能會認為如果 10 個執行緒併發執行Go
,最終_x
會為0
。然而,這並不一定,因為可能存在競態條件(race condition),在一個執行緒完成讀取x
的當前值,減少值,把值寫回這個過程之間,被另一個執行緒搶佔(導致一個過期的值被寫回)。
當然,可以通過用lock
語句封裝非原子的操作來解決這些問題。實際上,鎖如果一致的使用,可以模擬原子性。然而,Interlocked
類為這樣簡單的操作提供了一個更方便更快的方案:
class Program
{
static long _sum;
static void Main()
{ // _sum
// 簡單的自增/自減操作:
Interlocked.Increment (ref _sum); // 1
Interlocked.Decrement (ref _sum); // 0
// 加/減一個值:
Interlocked.Add (ref _sum, 3); // 3
// 讀取64位欄位:
Console.WriteLine (Interlocked.Read (ref _sum)); // 3
// 讀取當前值並且寫64位欄位
// (列印 "3",並且將 _sum 更新為 10 )
Console.WriteLine (Interlocked.Exchange (ref _sum, 10)); // 10
// 僅當欄位的當前值匹配特定的值(10)時才更新它:
Console.WriteLine (Interlocked.CompareExchange (ref _sum,
123, 10); // 123
}
}
Interlocked
上的所有方法都使用全柵欄。因此,通過Interlocked
訪問欄位不需要額外的柵欄,除非它們在程式其它地方沒有通過Interlocked
或lock
來訪問。
Interlocked
的數學運算操作僅限於Increment
、Decrement
以及Add
。如果你希望進行乘法或其它計算,在無鎖方式下可以使用CompareExchange
方法(通常與自旋等待一起使用)。我們會在並行程式設計中提供一個例子。
Interlocked
類通過將原子性的需求傳達給作業系統和虛擬機器來進行實現其功能。
Interlocked
類的方法通常產生 10ns 的開銷,是無競爭鎖的一半。此外,因為它們不會導致阻塞,所以不會帶來上下文切換的開銷。然而,如果在迴圈中多次迭代使用Interlocked
,就可能比在迴圈外使用一個鎖的效率低(不過Interlocked
可以實現更高的併發度)。