这个双重检查锁定需要使用volatile关键字吗?

3

可能是重复问题:
.NET中双重检查锁定需要volatile修饰符吗?

给定以下代码片段,可以由多个线程同时执行,是否需要使用volatile关键字来确保始终从内存而不是缓存中读取connected的“真实值”?

(假设Disconnect()只会被调用一次(即使第一次无法正常工作并且将connected的值读取为false,也不会再尝试)。

public class MyClass
{
    private readonly object syncRoot = new object();

    private bool connected; 

    public void Disconnect()
    {
        if (connected)
        {
            lock (syncRoot)
            {
                if (connected)
                {
                    // log off here
                    // ...
                    connected = false;
                }
            }
        }
    }

    public void Connect()
    {
        lock (syncRoot)
        {
            // blah
            // blah
            connected = true;
        }
    }
}

我的感觉是,如果使用双重检查锁定,则需要将其标记为volatile,因为如果第一次读取了不正确的值,那么它将认为它实际上已断开连接,并且不会进入锁定语句。我还认为,在这种情况下,双重检查锁定不合适/不会提供任何性能增益,只需使用普通锁即可完成工作。 我希望有人能够确认或否认这些想法。

我已经链接了一篇我认为非常有帮助的文章,以更好地理解同步问题。简短的答案是,“lock”语句在读取和写入时隐式创建内存屏障,这实际上比volatile更强制执行。http://www.albahari.com/threading/part4.aspx - Dan Bryant
@Henk - 谢谢 - 这很相似,但在那个问题中它可以是空引用,也可以是一个引用,但不会再次设置为null。我将编辑此问题以添加Connect方法。 - GarethD
我投票支持重新开放这个问题。这并不完全等同于双重检查锁定模式。这段代码有两条不同的执行路径(通过不同的方法调用),而且还涉及到可能会泄漏的非托管资源,这使得分析变得更加复杂。我并不认为“volatile”可以使这段代码变得安全。 - Brian Gideon
@Brian:volatile甚至都不需要。lock和初始值(false)就足够了。这两个代码路径只是轻微的变化。 - H H
3个回答

1

我理解 volatile 的作用是:你确实需要它。我认为,使用 volatile 关键字能够强制每次都从内存中读取变量值,并且可以避免编译器对该值进行缓存或执行其他可能导致未能正确读取“正确”值的优化。

请参见Eric Lippert的答案,他提到了使用 volatile 读操作。


不,你不想使用volatile。 - H H
@Henk, 你能解释一下为什么你不想使用volatile吗(注意:你提供的链接已经失效了)? 我知道这很难搞对,但我的印象是,在这种情况下,你确实想使用它。 - pstrjds
volatile 已被(非正式地)弃用。在这里 lock 提供了所有必要的屏障(见@Dan的评论)。该链接没有问题,但该网站现在已经关闭。同时请查看 https://dev59.com/PG025IYBdhLWcg3wLizo#6164770。 - H H
@Henk,我没有看到@Dan的任何评论,但是@Hans的写作很不错。所以基本上锁定保证了读取和写入不会被重新排序。但是如果Connect和Disconnect中都有双重检查锁定,似乎编译器导致布尔值缓存可能会导致死锁或导致连接保持打开或将来无法打开。 - pstrjds
该评论位于问题下方。您的死锁/泄漏场景需要在Connect()中至少有一个dcl,并且connected需要有一个错误的起始值。 - H H

1

如果Connect()方法也使用了lock (syncRoot),那么代码应该可以在没有volatile的情况下正常工作。

但是你在这里不会看到dcl的任何好处,为什么要冒险而费力呢?


谢谢。您能详细解释一下“代码应该工作”的含义吗?为什么代码应该工作,以及为什么不需要使用 volatile 关键字?请注意,连接变量的第一个检查不在 lock() 语句内部。 - GarethD
@Gareth:但是它在Connect()函数内部被包含在一个lock中,对吧?这就足够了。 - H H
是的,Connect方法也在锁定内设置了它,但我的感觉是,如果其他东西在没有锁定的情况下读取它,那么这并没有什么帮助? - GarethD
@Gareth:跟随不同的链接,但基本上Connect()中的锁将确保Disconnect()不会看到过时的值。 - H H
我看不到有哪里提到这一点。假设线程A正在调用Connect(),它正在使用syncRoot对象进行同步。线程B调用了Disconnect(),第一次读取connected时没有在锁定内。假设它将值(错误地)读取为false,该方法将简单地返回。我对Monitor.Enter/Exit的理解是线程必须相互配合。如果MyClass本身用于同步,可能会有所不同。您可以编辑您的答案并引用文章中具体阐述此事的部分吗? - GarethD
显示剩余3条评论

-2

你是正确的,volatile关键字从内存中读取而不是CPU缓存。但这并不意味着它是线程安全的。将syncLock变量更改为静态修饰符,以确保在多个实例之间线程安全。这是推荐的方法。


1
保证只有一个实例,而且这也不是问题的关键。 - GarethD
此外,被比较的值只是一个单独的实例,如果该值是静态的,则需要使用静态锁。 - pstrjds
1
synRoot 应该与 connected 一样静态。 - H H
1
@Dominic - 第一句话以问号结尾。我不关心syncRoot变量不是静态的这个事实 - 这不是问题所在。 - GarethD
2
在C#规范的第10.5.3节中,确切地在哪里说明了"volatile"保证了"从内存读取而不是CPU缓存"? 我完全没有看到任何证据证明这种说法是正确的。 - Eric Lippert
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接