Interlocked 是不是在所有线程中提供可见性?
Posted
技术标签:
【中文标题】Interlocked 是不是在所有线程中提供可见性?【英文标题】:Does Interlocked provide visibility in all threads?Interlocked 是否在所有线程中提供可见性? 【发布时间】:2010-10-05 17:40:09 【问题描述】:假设我有一个变量“counter”,并且有多个线程使用 Interlocked 访问和设置“counter”的值,即:
int value = Interlocked.Increment(ref counter);
和
int value = Interlocked.Decrement(ref counter);
我可以假设,Interlocked 所做的更改将在所有线程中可见吗?
如果没有,我应该怎么做才能让所有线程同步变量?
编辑:有人建议我使用 volatile。但是当我将“计数器”设置为 volatile 时,会出现编译器警告“对 volatile 字段的引用不会被视为 volatile”。
当我阅读在线帮助时,它说“通常不应使用 ref 或 out 参数传递 volatile 字段”。
【问题讨论】:
是的,互锁递增/递减(在 x86 和 IA-64 上)自动提供所有线程的可见性,因为它具有隐式内存屏障。挥发性不是必需的(尽管它不是非法的)。 【参考方案1】:我可以假设,Interlocked 所做的更改将在所有线程中可见吗?
这取决于您如何读取该值。如果您“只是”阅读它,那么不,除非您将其标记为易失性,否则它不会在其他线程中始终可见。不过,这会导致令人讨厌的警告。
作为替代方案(也是 IMO 的首选),请使用另一个 Interlocked 指令读取它。这将始终在所有线程上看到更新的值:
int readvalue = Interlocked.CompareExchange(ref counter, 0, 0);
返回读取的值,如果为 0,则将其与 0 交换。
动机:警告暗示某事不正确;将这两种技术(易失性和互锁性)结合起来并不是这样做的预期方式。
更新:似乎另一种不使用“易失性”的可靠 32 位读取的方法是使用 Thread.VolatileRead
,正如 this answer 中所建议的那样。还有一些证据表明我完全错误地将 Interlocked
用于 32 位读取,例如 this Connect issue,尽管我想知道这种区别本质上是否有点迂腐。
我真正的意思是:不要将此答案用作您的唯一来源;我对此表示怀疑。
【讨论】:
【参考方案2】:x86 CPU 上的 InterlockedIncrement/Decrement(x86 的加锁/减锁)会自动创建 内存屏障,这让所有线程都可以看到(即,所有线程都可以按顺序看到其更新,如顺序内存一致性)。内存屏障使所有待处理的内存加载/存储完成。 volatile
与这个问题无关,尽管 C# 和 Java(以及一些 C/C++ 编译器)强制使用 volatile
来制造内存屏障。但是,联锁操作已经有 CPU 的内存屏障。
还请查看 *** 中的 my another answer。
请注意,我假设 C# 的 InterlockedIncrement/Decrement 是到 x86 的锁加/减的内在映射。
【讨论】:
仅硬件可见性不足以暗示“程序”可见性。【参考方案3】:实际上,它们不是。如果您想安全地修改counter
,那么您做的是正确的事情。但是如果你想直接阅读counter
,你需要将它声明为volatile
。否则,编译器没有理由相信 counter
会改变,因为 Interlocked
操作在它可能看不到的代码中。
【讨论】:
这是正确的,尽管volatile
不是唯一可用的方法。 Volatile.Read
可能更合适。【参考方案4】:
Interlocked 确保一次只有 1 个线程可以更新值。为了确保其他线程可以读取正确的值(而不是缓存值),请将其标记为 volatile。
public volatile int 计数器;
【讨论】:
当我标记为 volatile 时,有编译器警告。 “对 volatile 字段的引用不会被视为 volatile”。 忽略这种情况下的警告:***.com/questions/425132/… 显然,如果您使用 Interlocked,则不需要 Volatile,但如果您在不使用 Interlocked 的情况下进行修改,则需要。 只是为了澄清。如果您要在不获取锁的情况下读取它们,请将它们标记为易失性。使用 Interlocked.Increment 来同步更新,或者在某些东西上使用 lock()。您收到的有关“ref 不被视为 volatile”的警告是通用的,在 Interlocked 的情况下可以忽略。 恐怕这不是正确的答案。任何其他线程都可以看到互锁操作。它对所有线程都有可见性。挥发性不是必需的。如果我错了,请纠正我。【参考方案5】:没有; Interlocked-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 寄存器中的值,并且] 程序现在卡在上面突出显示的循环中。 .
在 while 循环中使用
lock
或添加Thread.MemoryBarrier
都可以解决问题。或者你甚至可以使用Volatile.Read
[或volatile
字段]。 这里内存屏障的目的只是为了抑制 JIT 优化。 现在我们已经了解了软件和硬件如何重新排序内存操作,是时候讨论内存屏障了..
也就是说,在读取端需要一个额外的 barrier 构造,以防止编译和 JIT 重新排序/优化出现问题:这是与内存一致性不同的问题!
在此处添加volatile
将阻止 JIT 优化,从而“解决问题”,即使这样会导致警告。也可以通过使用Volatile.Read
或导致障碍的各种其他操作之一来纠正此程序:这些障碍与底层硬件内存栅栏一样是 CLR/JIT 程序正确性的一部分。
【讨论】:
以上是关于Interlocked 是不是在所有线程中提供可见性?的主要内容,如果未能解决你的问题,请参考以下文章
使用Interlocked在多线程下进行原子操作,无锁无阻塞的实现线程运行状态判断