Interlocked是否提供所有线程的可见性?

时间:2009-02-05 17:10:02

标签: c# multithreading visibility interlocked

假设我有一个变量“counter”,并且有几个线程通过使用Interlocked来访问和设置“counter”的值,即:

int value = Interlocked.Increment(ref counter);

int value = Interlocked.Decrement(ref counter);

我可以假设,Interlocked所做的更改将在所有线程中可见吗?

如果没有,我该怎么做才能使所有线程同步变量?

编辑:有人建议我使用volatile。但是当我将“计数器”设置为volatile时,会出现编译器警告“对volatile字段的引用不会被视为volatile。”

当我阅读在线帮助时,它说:“通常不应使用ref或out参数传递易失性字段。”

5 个答案:

答案 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程序的正确性一样,都是底层硬件内存的一部分。