简体   繁体   English

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

[英]Is volatile still needed inside lock statements?

I have read at different places people saying one should always use lock instead of volatile . 我在不同的地方读过人们说应该总是使用lock而不是volatile I found that there are lots of confusing statements about Multithreading out there and even experts have different opinions on some things here. 我发现在那里有很多关于多线程的令人困惑的陈述,甚至专家对这里的一些事情也有不同的看法。

After lots of research I found that the lock statement also will insert MemoryBarriers at least. 经过大量研究后,我发现lock语句至少也会插入MemoryBarriers

For example: 例如:

    public bool stopFlag;

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

But if I am not totally wrong, the JIT compiler is free to never actually read the variable inside the loop but instead it may only read a cached version of the variable from a register . 但是,如果我不是完全错误的话,JIT编译器可以自由地从不实际读取循环内的变量,而是只能从寄存器中读取变量的缓存版本 AFAIK MemoryBarriers won't help if the JIT made a register assignment to a variable, it just ensures that if we read from memory that the value is current . 如果JIT对变量进行寄存器赋值,AFAIK MemoryBarriers将无济于事,它只是确保如果我们从内存中读取该值是当前的

Unless there is some compiler magic saying something like "if a code block contains a MemoryBarrier, register assignment of all variables after the MemoryBarrier is prevented". 除非有一些编译器魔术说“如果代码块包含一个MemoryBarrier,在MemoryBarrier被阻止后注册所有变量”。

Unless declared volatile or read with Thread.VolatileRead() , if myBool is set from another Thread to false , the loop may still run infinite, is this correct? 除非声明volatile或使用Thread.VolatileRead()读取,如果myBool从另一个Thread设置为false ,循环可能仍然无限运行,这是正确的吗? If yes, wouldn't this apply to ALL variables shared between Threads? 如果是,那么这不适用于Threads之间共享的所有变量吗?

the JIT compiler is free to never actually read the variable inside the loop but instead it may only read a cached version of the variable from a register. JIT编译器可以自由地实际读取循环中的变量,但它只能从寄存器中读取变量的缓存版本。

Well, it'll read the variable once , in the first iteration of the loop, but other than that, yes, it will continue to read a cached value, unless there is a memory barrier. 好吧,它会在循环的第一次迭代中读取变量一次 ,但除此之外,是的,除非存在内存障碍,否则它将继续读取缓存值。 Any time the code crosses a memory barrier it cannot use the cached value. 每当代码穿过内存屏障时,它都不能使用缓存的值。

Using Thread.VolatileRead() adds the appropriate memory barriers, as does marking the field as volatile . 使用Thread.VolatileRead()添加适当的内存屏障,将字段标记为volatile There are plenty of other things that one could do that also implicitly add memory barriers; 人们还可以做很多其他的事情,也暗中增加了记忆障碍; one of them is entering or leaving a lock statement.` 其中一个是进入或离开lock声明

Since your loop is saying within the body of a single lock and not entering or leaving it, it's free to continue using the cached value. 由于你的循环是在一个lock的主体内说,而不是进入或离开它,它可以继续使用缓存的值。

Of course, the solution here isn't to add in a memory barrier. 当然,这里的解决方案不是添加内存屏障。 If you want to wait for another thread to notify you of when you should continue on, use a AutoResetEvent (or another similar synchronization tool specifically designed to allow threads to communicate). 如果您想等待另一个线程通知您何时应该继续,请使用AutoResetEvent (或专门设计为允许线程进行通信的其他类似同步工具)。

Whenever I see a question like this, my knee-jerk reaction is "assume nothing!" 每当我看到这样的问题时,我的下意识反应就是“什么都不做!” The .NET memory model is quite weak and the C# memory model is especially noteworthy for using language that can only apply to a processor with a weak memory model that isn't even supported anymore. .NET内存模型相当薄弱,C#内存模型特别值得注意的是使用的语言只能应用于具有甚至不再支持的弱内存模型的处理器。 Nothing what's there tells you anything what's going to happen in this code, you can reason about locks and memory barriers until you're blue in the face but you don't get anywhere with it. 什么都没有告诉你任何事情会在这段代码中发生什么,你可以解释锁和记忆障碍,直到你脸上的蓝色,但你没有随处可见。

The x64 jitter is quite clean and rarely throws a surprise. x64的抖动非常干净,很少会引起惊喜。 But its days are numbered, it is going to be replaced by Ryujit in VS2015. 但它的日子已经屈指可数,它将在VS2015中由Ryujit取代。 A rewrite that started with the x86 jitter codebase as a starting point. 以x86 jitter代码库为起点的重写。 Which is a concern, the x86 jitter can throw you for a loop. 这是一个问题,x86抖动可以引发你的循环。 Pun intended. 双关语意图。

Best thing to do is to just try it and see what happens. 最好的办法就是尝试一下,看看会发生什么。 Rewriting your code a little bit and making that loop as tight as possible so the jitter optimizer can do anything it wants: 稍微重写您的代码并使该循环尽可能紧密,以便抖动优化器可以执行任何想要的操作:

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

And testing it like this: 并测试它像这样:

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

Switch to the Release build. 切换到发布版本。 Project + Properties, Build tab, tick the "Prefer 32-bit" option. 项目+属性,构建选项卡,勾选“首选32位”选项。 Tools + Options, Debugging, General, untick the "Suppress JIT optimization" option. 工具+选项,调试,常规,取消勾选“抑制JIT优化”选项。 First run the Debug build. 首先运行Debug构建。 Works fine, program terminates after a second. 工作正常,程序在一秒钟后终止。 Now switch to the Release build, run and observe that it deadlocks, the loop never completes. 现在切换到Release版本,运行并观察它是死锁,循环永远不会完成。 Use Debug + Break All to see that it hangs in the loop. 使用Debug + Break All可以看到它在循环中挂起。

To see why, look at the generated machine code with Debug + Windows + Disassembly. 要了解原因,请使用Debug + Windows + Disassembly查看生成的机器代码。 Focusing on the loop only: 仅关注循环:

                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;

The instruction at address 013E26E8 tells the tale. 地址013E26E8的指令讲述了故事。 Note how the myBool variable is stored in the eax register, cnt in the edx register. 注意myBool变量如何存储在edx寄存器中的eax寄存器cnt中。 A standard duty of the jitter optimizer, using the processor registers and avoiding memory loads and stores makes the code much faster. 抖动优化器的标准任务,使用处理器寄存器并避免存储器加载和存储使代码更快。 And note that when it tests the value, it still uses the register and does not reload from memory. 并且记住,当测试值,它仍然采用了寄存器不会从内存中加载。 This loop can therefore never end and it will always hang your program. 因此,此循环永远不会结束,它将始终挂起您的程序。

Code is pretty fake of course, nobody will ever write this. 代码很可鄙,当然没有人会写这个。 In practice this tends to work by accident, you'll have more code inside the while() loop. 在实践中,这往往是偶然的,你会在while()循环中有更多的代码。 Too much to allow the jitter to optimize the variable way entirely. 太多,以允许抖动完全优化变量方式。 But there are no hard rules that will tell you when this happens. 但是,没有硬性规则可以告诉你何时发生这种情况。 Sometimes it does pull it off, assume nothing . 有时它确实会把它拉下来, 什么都不做 Proper synchronization should never be skipped. 绝不应跳过正确的同步。 You really are only safe with an extra lock for myBool or an ARE/MRE or Interlocked.CompareExchange(). 你真的只为myBool或ARE / MRE或Interlocked.CompareExchange()提供额外的锁定。 And if you want to cut such a volatile corner then you must check. 如果你想削减这样一个不稳定的角落,你必须检查。

And noted in the comments, try Thread.VolatileRead() instead. 并在注释中注明,请尝试使用Thread.VolatileRead()。 You need to use a byte instead of a bool . 你需要使用一个字节而不是一个bool It still hangs, it is not a synchronization primitive. 它仍然挂起,它不是同步原语。

how about this 这个怎么样

public class Singleton<T> where T : class, new()
{
    private static T _instance;
    private static object _syncRoot = new Object();

    public static T Instance
    {
        get
        {
            var instance = _instance;
            if (instance == null)
            {
                lock (_syncRoot)
                {
                    instance = Volatile.Read(ref _instance);
                    if (instance == null)
                    {
                        instance = new T();
                    }
                    Volatile.Write(ref _instance, instance);
                }
            }
            return instance;
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM