在锁定语句中是否还需要volatile?

cod*_*nix 3 c# multithreading locking volatile

我在不同的地方读过人们说应该总是使用lock而不是volatile.我发现在那里有很多关于多线程的令人困惑的陈述,甚至专家对这里的一些事情也有不同的看法.

经过大量研究后,我发现锁定语句也MemoryBarriers至少会插入.

例如:

    public bool stopFlag;

    void Foo()
    {
        lock (myLock)
        {
          while (!stopFlag)
          {
             // do something
          }
        }
    }
Run Code Online (Sandbox Code Playgroud)

但是,如果我不是完全错误的话,JIT编译器可以自由地从不实际读取循环内的变量,而是只能从寄存器中读取变量的缓存版本.如果JIT对变量进行寄存器赋值,AFAIK MemoryBarriers将无法帮助,它只是确保如果我们从内存中读取该值是当前的.

除非有一些编译器魔术说"如果代码块包含一个MemoryBarrier,在MemoryBarrier被阻止后注册所有变量".

除非声明volatile或读取Thread.VolatileRead(),如果myBool从另一个Thread设置false,循环可能仍然无限运行,这是正确的吗?如果是,那么这不适用于Threads之间共享的所有变量吗?

Ser*_*rvy 6

JIT编译器可以自由地实际读取循环中的变量,但它只能从寄存器中读取变量的缓存版本.

好吧,它会在循环的第一次迭代中读取变量一次,但除此之外,是的,除非存在内存障碍,否则它将继续读取缓存值.每当代码穿过内存屏障时,它都不能使用缓存的值.

使用Thread.VolatileRead()添加适当的内存屏障,将字段标记为volatile.人们还可以做很多其他的事情,也暗中增加了记忆障碍; 其中一人正在进入或离开一份lock声明

由于你的循环是在单个体内说的lock而不是进入或离开它,所以可以继续使用缓存的值.

当然,这里的解决方案不是添加内存屏障.如果您想等待另一个线程通知您何时应该继续,请使用AutoResetEvent(或专门设计为允许线程进行通信的其他类似同步工具).


Han*_*ant 6

每当我看到这样的问题时,我的下意识反应就是"什么都不做!" .NET内存模型相当薄弱,C#内存模型特别值得注意的是使用的语言只能应用于具有甚至不再支持的弱内存模型的处理器.什么都没有告诉你任何事情会在这段代码中发生什么,你可以解释锁和记忆障碍,直到你脸上的蓝色,但你没有随处可见.

x64的抖动非常干净,很少会引起惊喜.但它的日子已经屈指可数,它将在VS2015中由Ryujit取代.以x86 jitter代码库为起点的重写.这是一个问题,x86抖动可以引发你的循环.双关语意图.

最好的办法就是尝试一下,看看会发生什么.稍微重写您的代码并使该循环尽可能紧密,以便抖动优化器可以执行任何想要的操作:

class Test {
    public bool myBool;
    private static object myLock = new object();
    public int Foo() {
        lock (myLock) {
            int cnt = 0;
            while (!myBool) cnt++;
            return cnt;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并测试它像这样:

    static void Main(string[] args) {
        var obj = new Test();
        new Thread(() => {
            Thread.Sleep(1000);
            obj.myBool = true;
        }).Start();
        Console.WriteLine(obj.Foo());
    }
Run Code Online (Sandbox Code Playgroud)

切换到发布版本.项目+属性,构建选项卡,勾选"首选32位"选项.工具+选项,调试,常规,取消勾选"抑制JIT优化"选项.首先运行Debug构建.工作正常,程序在一秒钟后终止.现在切换到Release版本,运行并观察它是死锁,循环永远不会完成.使用Debug + Break All可以看到它在循环中挂起.

要了解原因,请使用Debug + Windows + Disassembly查看生成的机器代码.仅关注循环:

                int cnt = 0;
013E26DD  xor         edx,edx                      ; cnt = 0
                while (myBool) {
013E26DF  movzx       eax,byte ptr [esi+4]         ; load myBool 
013E26E3  test        eax,eax                      ; myBool == true?
013E26E5  jne         013E26EC                     ; yes => bail out
013E26E7  inc         edx                          ; cnt++
013E26E8  test        eax,eax                      ; myBool == true?
013E26EA  jne         013E26E7                     ; yes => loop
                }
                return cnt;
Run Code Online (Sandbox Code Playgroud)

地址013E26E8的指令讲述了故事.注意myBool变量如何存储在edx寄存器中的eax寄存器cnt中.抖动优化器的标准任务,使用处理器寄存器并避免存储器加载和存储使代码更快.并且记住,当测试值,它仍然采用了寄存器并没有从内存中加载.因此,此循环永远不会结束,它将始终挂起您的程序.

代码很可鄙,当然没有人会写这个.在实践中,这往往是偶然的,你会在while()循环中有更多的代码.太多,以允许抖动完全优化变量方式.但是,没有硬性规则可以告诉你何时发生这种情况.有时它确实会把它拉下来,什么都不做.绝不应跳过正确的同步.你真的只为myBool或ARE/MRE或Interlocked.CompareExchange()提供额外的锁定.如果你想削减这样一个不稳定的角落,你必须检查.

并在注释中注明,请尝试使用Thread.VolatileRead().你需要使用一个字节而不是一个bool.它仍然挂起,它不是同步原语.