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在多线程下进行原子操作,无锁无阻塞的实现线程运行状态判断

Interlocked介绍

Interlocked介绍

《C#高级编程》读书笔记(十五):任务线程和同步之四 同步

在 C# 中使用 Interlocked 获取和设置线程安全吗?

Interlocked