template boy
template boy

Reputation: 10490

What makes a singleton thread-unsafe?

I read somewhere that a singleton was thread-unsafe. I'm trying to understand why this is. If I have a singleton object like this:

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

And if I have code like this:

singleton *p1, *p2;

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

t1.join();
t2.join();

Is it possible for p1 and p2 to point to two different singleton instances? If unique is static, does its "static" nature not take affect until it's fully initialized? If that's so, does that mean that a static object's initialization can be accessed concurrently and thus allowing the creation of multiple static objects?

Upvotes: 1

Views: 168

Answers (1)

Howard Hinnant
Howard Hinnant

Reputation: 219428

In C++98/03 a file local static:

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

meant that your code would do something like this:

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);
}

Where the "__"-prefixed names are compiler supplied.

But in the above code, nothing is stopping two threads from entering the if at the same time, and both trying to construct the X at the same time. The compiler might try to combat that problem by writing:

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);
}

But now it is possible for one thread to set __instance_initialized to true and start constructing the X, and have the second thread test and skip over the if while the first thread is still busy constructing X. The second thread would then present uninitialized memory to its client until the first thread finally completes the construction.

In C++11 the language rules were changed such that the compiler must set up the code such that the second thread can not run past, nor start the construction of X until the first thread successfully finishes the construction. This may mean that the second thread has to wait an arbitrary amount of time before it can proceed ... until the first thread finishes. If the first thread throws an exception while trying to construct X, the second thread will wake up and try its hand at constructing it.

Here is the Itanium ABI specification for how the compiler might accomplish that.

Upvotes: 11

Related Questions