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

时间:2015-04-09 16:31:44

标签: c# multithreading locking volatile

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

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

例如:

    public bool stopFlag;

    void Foo()
    {
        lock (myLock)
        {
          while (!stopFlag)
          {
             // do something
          }
        }
    }

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

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

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

3 个答案:

答案 0 :(得分:6)

  

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

嗯,它会在循环的第一次迭代中读取变量一次,但除此之外,是的,它会继续读取缓存的值,除非有一个记忆障碍。只要代码穿过内存屏障,它就不能使用缓存的值。

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

由于您的循环是在单个lock的正文中说明而不是进入或离开它,因此可以继续使用缓存的值。

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

答案 1 :(得分: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;
        }
    }
}

并按照以下方式进行测试:

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

切换到发布版本。项目+属性,构建选项卡,勾选“首选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;

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

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

在评论中注明,请尝试使用Thread.VolatileRead()。您需要使用字节而不是 bool 。它仍然挂起,它不是同步原语。

答案 2 :(得分:0)

这个怎么样

CultureInfo modified = CultureInfo.CurrentCulture.Clone() as CultureInfo;

modified.NumberFormat.NumberDecimalSeparator = ".";

// Business as usual except the decimal separator which is now dot '.' 
Thread.CurrentThread.CurrentCulture = modified;

...

double result = double.Parse(source);