是什么使单例线程不安全

What makes a singleton thread-unsafe?

本文关键字:不安全 线程 单例 是什么      更新时间:2023-10-16

我在某处读到单例是线程不安全的。我试图理解为什么会这样。如果我有一个这样的单例对象:

class singleton final
{
public:
    static singleton& instance()
    {
        static singleton unique;
        return unique;
    }
private:
    singleton() = default;
    singleton(singleton const&) = delete;
    singleton& operator=(singleton const&) = delete;
};

如果我有这样的代码:

singleton *p1, *p2;
auto t1 = std::thread([] { p1 = &singleton::instance(); });
auto t2 = std::thread([] { p2 = &singleton::instance(); });
t1.join();
t2.join();

p1p2 是否可以指向两个不同的singleton实例?如果uniquestatic,它的"静态"性质在完全初始化之前不会生效吗?如果是这样,这是否意味着可以并发访问静态对象的初始化,从而允许创建多个静态对象?

在 C++98/03 中,一个文件本地静态:

X& instance()
{
    static X x;
    return x;
}

意味着您的代码将执行以下操作:

bool __instance_initialized = false;
alignas(X) char __buf_instance[sizeof(X)];
// ...
X& instance()
{
    if (!__instance_initialized)
    {
        ::new(__buf_instance) X;
        __instance_initialized = true;
    }
    return *static_cast<X*>(__buf_instance);
}

其中提供了以"__"为前缀的名称编译器。

但是在上面的代码中,没有什么能阻止两个线程同时进入if,并且都试图同时构造X。 编译器可能会尝试通过编写以下内容来解决此问题:

bool __instance_initialized = false;
alignas(X) char __buf_instance[sizeof(X)];
// ...
X& instance()
{
    if (!__instance_initialized)
    {
        __instance_initialized = true;
        ::new(__buf_instance) X;
    }
    return *static_cast<X*>(__buf_instance);
}

但是现在一个线程可以将__instance_initialized设置为 true 并开始构造X,并在第一个线程仍在忙于构造X时进行第二个线程测试并跳过if。 然后,第二个线程将向其客户端提供未初始化的内存,直到第一个线程最终完成构造。

在 C++11 中,语言规则发生了变化,编译器必须设置代码,使第二个线程不能运行过去,也不能在第一个线程成功完成构造之前开始构造X。 这可能意味着第二个线程必须等待任意时间才能继续......直到第一个线程完成。 如果第一个线程在尝试构造X时抛出异常,则第二个线程将唤醒并尝试构造它。

以下是 Itanium ABI 规范,用于说明编译器如何实现这一点。