这个双重检查锁定修复有什么问题?

时间:2009-06-03 14:50:24

标签: multithreading locking singleton sequence-points double-checked-locking

所以我看到很多文章现在声称在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;
    }
};

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

saw a suggestion使用线程本地布尔值并检查而不是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;
    }
};

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

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在一个线程被另一个线程分配时被读取,垃圾值仍然是非零的,因此是真的。 This Dr. Dobb's article声称你必须锁定,因为你永远不会在重新排序指令的情况下赢得与编译器的争斗。这让我觉得这不能出于某种原因,但我无法弄清楚为什么。如果序列点的要求与Dobb博士的文章让我相信的那样失败,我不明白为什么任何代码在锁之后无法重新排序。这将使C ++多线程破碎。

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

这样做还是不行呢?为什么呢?

3 个答案:

答案 0 :(得分:5)

您的修复程序无法解决任何问题,因为对sync_check和instance的写入可以在CPU上无序执行。作为一个例子,假设实例的前两次调用几乎同时发生在两个不同的CPU上。第一个线程将获取锁,初始化指针并按顺序将sync_check设置为true,但处理器可能会更改写入内存的顺序。在另一个CPU上,第二个线程可以检查sync_check,看它是否为真,但实例可能尚未写入内存。有关详细信息,请参阅Lockless Programming Considerations for Xbox 360 and Microsoft Windows

您提到的特定于线程的sync_check解决方案应该可以正常工作(假设您将指针初始化为0)。

答案 1 :(得分:1)

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

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

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

顺便说一句,我已经在.net 2.0中读过,双重检查锁定可以保证在没有“volatile”变量的情况下工作(对于那里的任何.net读者) - 这对你的c ++代码没有帮助

如果你想要安全,你需要做c ++相当于在c#中将变量标记为volatile。

答案 2 :(得分:0)

“后一种情况打破了这个习惯用语 - 两个线程最终可能会创建单例。”

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

创建实例并解锁互斥锁后,下一个等待的线程将锁定互斥锁,但它不会尝试创建新实例,因为检查将失败。

下次检查实例变量时,它将被设置,因此没有线程会尝试创建新实例。

我不确定一个线程将新实例指针分配给实例而另一个线程检查同一个变量的情况 - 但我相信在这种情况下它会被正确处理。

我在这里错过了什么吗?

好不确定操作的重新排序,但在这种情况下,它会改变逻辑,所以我不希望它发生 - 但我不是这个主题的专家。