Double-Checked Locking Pattern in C++11?
Asked Answered
F

3

11

The new machine model of C++11 allows for multi-processor systems to work reliably, wrt. to reorganization of instructions.

As Meyers and Alexandrescu pointed out the "simple" Double-Checked Locking Pattern implementation is not safe in C++03

Singleton* Singleton::instance() {
  if (pInstance == 0) { // 1st test
    Lock lock;
    if (pInstance == 0) { // 2nd test
      pInstance = new Singleton;
    }
  }
  return pInstance;
}

They showed in their article that no matter what you do as a programmer, in C++03 the compiler has too much freedom: It is allowed to reorder the instructions in a way that you can not be sure that you end up with only one instance of Singleton.

My question is now:

  • Do the restrictions/definitions of the new C++11 machine model now constrain the sequence of instructions, that the above code would always work with a C++11 compiler?
  • How does a safe C++11-Implementation of this Singleton pattern now looks like, when using the new library facilities (instead of the mock Lock here)?
Forbes answered 15/5, 2011 at 13:38 Comment(3)
Use a Singleton- get what you pay for.Publia
Also, don't forget that C++0x now guarantees initialization of static variables to be thread-safe. See §6.7/4: If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization; i.e. you could use something like static Singleton* ptr = new Singleton(); return ptr;.Slavish
@Vitus: Indeed. But it doesn't tell where you pay for the lock, then. The formulation clearly requires some kind of lock around the static variable, and it is specifically that lock that Double-Checked Locking is trying to avoid.Forbes
O
5

If pInstance is a regular pointer, the code has a potential data race -- operations on pointers (or any builtin type, for that matter) are not guaranteed to be atomic (EDIT: or well-ordered)

If pInstance is an std::atomic<Singleton*> and Lock internally uses an std::mutex to achieve synchronization (for example, if Lock is actually std::lock_guard<std::mutex>), the code should be data race free.

Note that you need both explicit locking and an atomic pInstance to achieve proper synchronization.

Overthrow answered 15/5, 2011 at 15:13 Comment(5)
Hmm, I wonder. The issue (Meyers/Alexandrescu) seems, the Compiler might reorder the instructions, so in Thread A the pInstance will get its value before Singleton is fully created, stopped, and Thread B sees the the filled pInstance and uses it. Does an atomic<Singleton*> really protect me there? I can not see that. I might need a fully-blown [Memory-Ordering][1]. [1][justsoftwaresolutions.co.uk/threading/…Forbes
@Forbes - Yes, it will. All operations on std::atomic<>s default to fully sequentially consistent memory ordering, and weaker orderings must be explicitly requested. Sequential consistency means that each thread sees everything happen in exactly the same order, as though all operations were fully serialized.Overthrow
@Forbes - To elaborate: this single total order is over all properly synchronized operations in the whole program, not only over the operations of individual atomic variables. Specifically, atomic operations synchronize with the mutex lock/unlock primitives, so that the comparison and locking in the above code may not be reordered. I think.Overthrow
I thought that atomic does not use a mutex. I was fairly certain it only guarantees that the compiler will output code in the right order and inserts a memory barrier to stop out of order execution.Taro
@Tim: That's right, the memory barrier is sufficient to cause synchronization with lock/unlock.Shielashield
T
4

Since static variable initialization is now guaranteed to be threadsafe, the Meyer's singleton should be threadsafe.

Singleton* Singleton::instance() {
  static Singleton _instance;
  return &_instance;
}

Now you need to address the main problem: there is a Singleton in your code.

EDIT: based on my comment below: This implementation has a major drawback when compared to the others. What happens if the compiler doesn't support this feature? The compiler will spit out thread unsafe code without even issuing a warning. The other solutions with locks will not even compile if the compiler doesn't support the new interfaces. This might be a good reason not to rely on this feature, even for things other than singletons.

Taro answered 5/9, 2013 at 13:22 Comment(2)
Yes indeed. By now this is probably common knowledge :-) Also, when the committee agreed on that feature I heard that they were not perfectly sure if and how much overhead this would cost -- even when not using in a multi-threading program. Therefore I was curious if paranoid people might try to circumvent static local variables for the singleton pattern because "it is faster" in 0.1% of the cases...Forbes
@Forbes I figure it is probably common knowledge also, but none of the other answers mentioned it, so I did. as of writing I believe GCC and Clang support it but MSVC does not. In one metric at least, this is the best solution: it has the least code, and is the simplest to understand. One caveat is that it might be wise to accompany this with a comment that it requires this feature. The other implementations will throw a compile error if they don't support the necessary features, but this will happily compile to incorrect code if compiler support is not there.Taro
P
1

C++11 doesn't change the meaning of that implementation of double-checked locking. If you want to make double-checked locking work you need to erect suitable memory barriers/fences.

Pym answered 15/5, 2011 at 13:44 Comment(1)
Ok. What's the Stdlibs solution there? Do I need atomic_xxx_fence() with its explicit aquire and release operations in there, or do I use a higher level interface from the Stdlib which already uses that?Forbes

© 2022 - 2024 — McMap. All rights reserved.