所以我现在看到很多文章声称在 C++ 上双重检查锁定(通常用于防止多个线程尝试初始化延迟创建的单例)已被破坏。正常的双重检查锁定代码如下所示:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;

public:
    static singleton & instance()
    {
        static singleton* instance;

        if(!instance)
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;
        }

        return *instance;
    }
};

问题显然是分配实例的行——编译器可以自由地分配对象,然后将指针分配给它,或者将指针设置到将分配的位置,然后分配它。后一种情况打破了习惯用法——一个线程可以分配内存并分配指针,但在进入睡眠状态之前不运行单例的构造函数——然后第二个线程将看到实例不为空并尝试返回它,尽管它还没有建成。

看到一个建议 使用线程局部布尔值并检查它而不是 instance. 。像这样的东西:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;
    static boost::thread_specific_ptr<int> _sync_check;

public:
    static singleton & instance()
    {
        static singleton* instance;

        if(!_sync_check.get())
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;

            // Any non-null value would work, we're really just using it as a
            // thread specific bool.
            _sync_check = reinterpret_cast<int*>(1);
        }

        return *instance;
    }
};

这样,每个线程最终都会检查实例是否已创建一次,但之后会停止,这会带来一些性能影响,但仍然不会像锁定每个调用那么糟糕。但是如果我们只使用本地静态布尔值呢?:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;

public:
    static singleton & instance()
    {
        static bool sync_check = false;
        static singleton* instance;

        if(!sync_check)
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;

            sync_check = true;
        }

        return *instance;
    }
};

为什么这不起作用?即使sync_check 在被分配到另一个线程中时由一个线程读取,垃圾值仍将是非零,因此为真。 这位博士。多布的文章 声称你必须锁定,因为你永远不会赢得与编译器关于重新排序指令的战斗。这让我觉得这一定是因为某种原因不起作用,但我不明白为什么。如果对序列点的要求像博士一样输了。多布的文章让我相信,我不明白为什么 任何 锁之后的代码无法重新排序为锁之前。这会让C++多线程断了时期。

我想我可以看到编译器被允许专门将sync_check重新排序到锁之前,因为它是一个局部变量(即使它是静态的,我们也不会返回指向它的引用或指针)——但这仍然可以解决通过使其成为静态成员(实际上是全局的)来代替。

那么这行得通还是行不通呢?为什么?

有帮助吗?

解决方案

您修复不能解决任何事情,因为在写入sync_check和实例都可以做出来的顺序在CPU上。作为一个例子想象前两个电话给实例在两个不同的CPU发生在大约相同的时间。第一线程将获取锁,指针并设置sync_check初始化为真,以该顺序,但是处理器可以改变写入存储器的顺序。而CPU则有可能在第二个线程检查sync_check,看到它是真实的,但例如可能还没有被写入存储器。请参见无锁编程注意事项为Xbox 360和Microsoft Windows 对于细节。

在提应该工作然后该线程特定sync_check溶液(假设初始化指针为0)。

其他提示

这里有一些关于此的很棒的读物(尽管它是面向 .net/c# 的): http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

归根结底,你需要能够告诉 CPU,它无法为该变量访问重新排序你的读/写(从最初的 Pentium 开始,如果 CPU 认为逻辑不受影响,它可以重新排序某些指令) ),并且它需要确保缓存是一致的(不要忘记这一点——我们开发人员假装所有内存只是一个平面资源,但实际上,每个 CPU 核心都有缓存,有些是不共享的(L1 ),有时可能会共享一些(L2))——您的初始化可能会写入主 RAM,但另一个核心可能在缓存中具有未初始化的值。如果没有任何并发​​语义,CPU 可能不知道它的缓存是脏的。

我不知道 C++ 方面,但在 .net 中,您可以将变量指定为 易失性的,以保护对其的访问(或者您可以使用 System.Threading 中的内存读/写屏障方法)。

顺便说一句,我读到,在 .net 2.0 中,双重检查锁定保证在没有“易失性”变量的情况下工作(对于任何 .net 读者来说)——这对您的 C++ 代码没有帮助。

如果你想安全,你需要在 C++ 中执行相当于在 C# 中将变量标记为 易失性的操作。

“后一种情况打破了成语 - 两个线程可能最终创建单”

但是,如果我理解正确的代码,第一个例子,你检查是否实例已经存在(可能由多个线程在同一时间内执行),如果没有一个线程获取的锁定它,它创建实例 - 只有一个线程可以在当时执行的创建。所有其他线程会被锁定并等待。

在实例被创建和互斥量被释放下一个等待线程将锁定互斥体,但它不会尝试创建新实例,因为该检查将失败。

实例变量,则它将被设定下一次所以没有线程将尝试创建新实例。

我不知道在哪里,而另一个线程检查同一变量一个线程分配新的实例指针实例的情况下 - 但我相信它会正确地在这种情况下进行处理。

我失去了一些东西在这里?

好了不知道操作的重新排序,但在这种情况下,它会改变逻辑,所以我不希望它发生 - 但我对这个话题没有专家

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top