当在加锁释放代码下读写字段时,使用内存屏障也不总是够用的,操作64位字段,增值,减量需要使用Interlocked类。Interlocked类也提供给了Exchange和CompareExchange方法,后者可以是锁模式下,使用一点额外的代码实现读写字段操作。
在潜在的处理器上,如果一个语句以单一可视的指令执行在处理器上,那么它本质上是原子性的。严格的原子性排除了抢占的可能性。对32位或更少位数的字段的简单读写总是原子性的,但只有在64运行环境中,对64位的字段操作才保证原子性,包含多个读写操作的语句不是原子性的。
class Atomicity
{
static int _x, _y;
static long _z;
static void Test()
{
long myLocal;
_x = 3; // 原子性
_z = 3; // Nonatomic on 32-bit environs (_z is 64 bits)
myLocal = _z; // Nonatomic on 32-bit environs (_z is 64 bits)
_y += _x; // Nonatomic (read AND write operation)
_x++; // Nonatomic (read AND write operation)
}
}
在32位环境上读写64位字段不是原子性的,因为它要执行2个分开的指令,每个对32位内存地址。所以,如果线程x读取64为值,而线程Y正在更新它,线程X可能以按位合并了新值和旧值。(撕裂的读取)。
编译器实现单目运算符类似x++通过读取变量,处理,然后回写。
看下面的代码
class ThreadUnsafe
{
static int _x = 1000;
static void Go() { for (int i = 0; i < 100; i++) _x--; }
}
暂时不考虑内存屏障的问题,你可能期望10个线程并发运行Go,——x最终为0.然而,这个不保证的,因为竞争条件可能在获取——x的当前值的时候,一个线程抢占另一个线程,减去它,然后回写,导致一个过期的值被回写。当然,你可以通过用lock语句来包裹这个非原子操作符对付这个问题。事实上,锁如果始终应用的话,模拟了原子性。但是Interlocked类提供了更加简单快速的解决方案。
class Program
{
static long _sum;
static void Main()
{ // _sum
// Simple increment/decrement operations:
Interlocked.Increment (ref _sum); // 1
Interlocked.Decrement (ref _sum); // 0
// Add/subtract a value:
Interlocked.Add (ref _sum, 3); // 3
// Read a 64-bit field:
Console.WriteLine (Interlocked.Read (ref _sum)); // 3
// Write a 64-bit field while reading previous value:
// (This prints "3" while updating _sum to 10)
Console.WriteLine (Interlocked.Exchange (ref _sum, 10)); // 10
// Update a field only if it matches a certain value (10):
Console.WriteLine (Interlocked.CompareExchange (ref _sum,
123, 10); // 123
}
}
所有的这些Interlocked的方法产生了一个full fence。因此,你通过Interlocked访问的字段不再需要额外的屏障,除非他们访问你程序中的其他没有使用锁的地方。Interlocked的数学操作符限制在Increment, Decrement, 和Add。如果你想乘,或者执行其他计算,您能通过CompareExchange方法(通常和spin-waiting联合)。
Interlocked的方法通常需要10纳秒,是uncontended锁的一半,此外,它不需要额外的上下文切换所带来的开销。另一面,使用Interlocked在多个递归上不如循环中获得一个单一的锁有效。尽管Interlocked能更多的并行。