假设我有一个变量“counter”,并且有几个线程通过使用Interlocked来访问和设置“counter”的值,即:
int value = Interlocked.Increment(ref counter);
和
int value = Interlocked.Decrement(ref counter);
我可以假设,Interlocked所做的更改将在所有线程中可见吗?
如果没有,我该怎么做才能使所有线程同步变量?
编辑:有人建议我使用volatile。但是当我将“计数器”设置为volatile时,会出现编译器警告“对volatile字段的引用不会被视为volatile。”当我阅读在线帮助时,它说:“通常不应使用ref或out参数传递易失性字段。”
答案 0 :(得分:6)
我可以假设,Interlocked所做的更改将在所有线程中可见吗?
这取决于您如何阅读该值。如果您“只是”读取它,那么不会,除非您将其标记为易失性,否则这在其他线程中并不总是可见。这会引起恼人的警告。
作为替代(并且非常优选的IMO),使用另一个Interlocked指令读取它。这将始终在所有线程上看到更新的值:
int readvalue = Interlocked.CompareExchange(ref counter, 0, 0);
返回读取的值,如果为0,则将其与0交换。
动机:警告暗示事情不对;结合这两种技术(易失性和互锁)并不是预期的方法。
更新:似乎另一种可靠的32位读取而不使用“volatile”的方法是使用this answer中建议的Thread.VolatileRead
。还有一些证据表明我使用Interlocked
进行32位读取是完全错误的,例如this Connect issue,但我不知道这种区别是否有点迂腐。
我的真正含义是:不要将此答案作为您的唯一来源;我对此表示怀疑。
答案 1 :(得分:6)
x86 CPU上的InterlockedIncrement / Decrement(x86的lock add / dec)会自动创建内存屏障,这样可以查看所有线程(即,所有线程都可以按顺序查看其更新,如顺序记忆一致性)。内存屏障可以完成所有挂起的内存加载/存储。 volatile
与此问题无关,尽管C#和Java(以及一些C / C ++编译器)强制执行volatile
来阻止内存。但是,联锁操作已经由CPU具有内存屏障。
请在stackoverflow中查看my another answer。
请注意,我假设C#的InterlockedIncrement / Decrement是x86的lock add / dec的内在映射。
答案 2 :(得分:3)
实际上,他们不是。如果您想安全地修改counter
,那么您正在做正确的事情。但是,如果您想直接阅读counter
,则需要将其声明为volatile
。否则,编译器没有理由相信counter
会发生变化,因为Interlocked
操作在代码中可能看不到。
答案 3 :(得分:1)
Interlocked确保一次只有一个线程可以更新该值。要确保其他线程可以读取正确的值(而不是缓存值),请将其标记为volatile。
public volatile int Counter;
答案 4 :(得分:1)
没有;一个仅在Inter -ed-at-Write-only 确保代码中的变量读取实际上是新鲜的; 一个无法正确读取字段的程序可能不是线程安全的,即使在强大的内存模型"下也是如此。这适用于分配给线程之间共享的字段的任何形式。
以下是由于JIT 而永远不会终止的代码示例。 (它已从Memory Barriers in .NET修改为针对该问题更新的可运行LINQPad程序。)
// Run this as a LINQPad program in "Release Mode".
// ~ It will never terminate on .NET 4.5.2 / x64. ~
// The program will terminate in "Debug Mode" and may terminate
// in other CLR runtimes and architecture targets.
class X {
// Adding {volatile} would 'fix the problem', as it prevents the JIT
// optimization that results in the non-terminating code.
public int terminate = 0;
public int y;
public void Run() {
var r = new ManualResetEvent(false);
var t = new Thread(() => {
int x = 0;
r.Set();
// Using Volatile.Read or otherwise establishing
// an Acquire Barrier would disable the 'bad' optimization.
while(terminate == 0){x = x * 2;}
y = x;
});
t.Start();
r.WaitOne();
Interlocked.Increment(ref terminate);
t.Join();
Console.WriteLine("Done: " + y);
}
}
void Main()
{
new X().Run();
}
来自Memory Barriers in .NET的解释:
这次是JIT,而不是硬件。很明显,JIT缓存了变量terminate的值[在EAX寄存器中,并且]程序现在卡在上面突出显示的循环中。
使用
lock
或在while循环中添加Thread.MemoryBarrier
可以解决问题。或者您甚至可以使用Volatile.Read
[或volatile
字段]。 此处内存屏障的目的只是为了抑制JIT优化。现在我们已经看到软件和硬件如何重新排序内存操作,是时候讨论内存障碍了。
也就是说,读取端需要一个额外的 barrier 构造来防止编译和JIT重新排序/优化的问题:这是一个与记忆一致性不同的问题!
在此处添加volatile
将阻止 JIT优化,从而解决问题',即使这样会导致警告。此程序也可以通过使用Volatile.Read
或导致障碍的各种其他操作之一来纠正:这些障碍与CLR / JIT程序的正确性一样,都是底层硬件内存的一部分。