5

因此,我现在看到很多文章声称在 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 在被分配到另一个线程时被一个线程读取,垃圾值仍然是非零的,因此为真。Dobb 博士的这篇文章声称您必须锁定,因为您永远不会在重新排序指令上与编译器进行战斗。这让我认为由于某种原因这一定行不通,但我不知道为什么。如果对序列点的要求像 Dobb 博士的文章让我相信的那样丢失,我不明白为什么锁之后的任何代码都不能重新排序到锁之前。这将使 C++ 多线程中断期。

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

那么这会起作用还是不会呢?为什么?

4

3 回答 3

5

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

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

于 2009-06-16T15:49:47.030 回答
1

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

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

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

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

如果您想安全,您将需要在 c# 中将变量标记为 volatile 的 c++ 等效项。

于 2009-06-03T15:05:43.170 回答
0

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

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

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

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

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

我在这里错过了什么吗?

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

于 2009-06-03T15:06:23.887 回答