10

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

int value = Interlocked.Increment(ref counter);

int value = Interlocked.Decrement(ref counter);

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

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

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

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

4

5 回答 5

7

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

这取决于您如何读取该值。如果您“只是”阅读它,那么不,除非您将其标记为易失性,否则它不会总是在其他线程中可见。但是,这会导致烦人的警告。

作为替代方案(并且更喜欢 IMO),请使用另一个 Interlocked 指令阅读它。这将始终在所有线程上看到更新的值:

int readvalue = Interlocked.CompareExchange(ref counter, 0, 0);

它返回读取的值,如果为 0,则将其与 0 交换。

动机:警告暗示某事不正确;结合这两种技术(易失性和互锁性)并不是这样做的预期方法。

更新:似乎另一种不使用“易失性”的可靠 32 位读取方法是按照此答案Thread.VolatileRead中的建议使用。还有一些证据表明我完全错误地使用32 位读取,例如这个 Connect issue,尽管我想知道这种区别本质上是否有点迂腐。Interlocked

我真正的意思是:不要将此答案用作您的唯一来源;我对此表示怀疑。

于 2010-01-26T16:08:13.080 回答
7

x86 CPU 上的 InterlockedIncrement/Decrement(x86 的锁加/减)会自动创建内存屏障,从而为所有线程提供可见性(即,所有线程都可以按顺序看到其更新,如顺序内存一致性)。内存屏障使所有待处理的内存加载/存储完成。volatile与此问题无关,尽管 C# 和 Java(以及一些 C/C++ 编译器)强制执行volatile内存屏障。但是,联锁操作已经有 CPU 的内存屏障。

另请查看我在stackoverflow中的另一个答案。

请注意,我假设 C# 的 InterlockedIncrement/Decrement 是到 x86 的锁加/减的内在映射。

于 2010-01-26T19:16:38.223 回答
3

事实上,他们不是。如果您想安全地修改counter,那么您正在做正确的事情。但是,如果您想counter直接阅读,则需要将其声明为volatile. 否则,编译器没有理由相信它counter会改变,因为Interlocked操作是在它可能看不到的代码中。

于 2009-02-05T17:20:43.643 回答
1

互锁确保一次只有 1 个线程可以更新值。为了确保其他线程可以读取正确的值(而不是缓存值),将其标记为 volatile。

公共 volatile int 计数器;

于 2009-02-05T17:10:44.600 回答
1

不; 单独的Interlocked-at-Write-Only 并不能确保代码中的变量读取实际上是新鲜的;即使在“强内存模型”下,不能正确读取字段的程序也可能不是线程安全的。这适用于任何形式的分配给线程之间共享的字段。

这是一个由于 JIT 而永远不会终止的代码示例。(它是从.NET 中的 Memory Barriers修改为针对该问题更新的可运行 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();
}

.NET 中内存屏障的解释:

这次是 JIT,而不是硬件。很明显,JIT 已经缓存了变量 terminate [在 EAX 寄存器中的值,并且] 程序现在卡在上面突出显示的循环中..

在 while 循环中使用 alock或添加 a都可以解决问题。Thread.MemoryBarrier或者你甚至可以使用Volatile.Read[or a volatilefield]。这里内存屏障的目的只是为了抑制 JIT 优化。现在我们已经了解了软件和硬件如何重新排序内存操作,是时候讨论内存屏障了。

也就是说,在读取端需要一个额外的屏障构造来防止编译和 JIT 重新排序/优化问题:这是与内存一致性不同的问题!

在此处添加volatile阻止JIT 优化,从而“解决问题”,即使这样会导致警告。该程序也可以通过使用Volatile.Read导致障碍的各种其他操作之一来纠正:这些障碍与底层硬件内存栅栏一样是 CLR/JIT 程序正确性的一部分。

于 2018-06-04T19:00:59.917 回答