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)?
towi
  • 21,587
  • 28
  • 106
  • 187
  • 2
    Use a Singleton- get what you pay for. – Puppy May 15 '11 at 13:44
  • 3
    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;`. – Vitus May 15 '11 at 17:23
  • 1
    @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. – towi May 16 '11 at 16:52

3 Answers3

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.

JohannesD
  • 13,802
  • 1
  • 38
  • 30
  • 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` really protect me there? I can not see that. I might need a fully-blown [Memory-Ordering][1]. [1][http://www.justsoftwaresolutions.co.uk/threading/multithreading-in-c++0x-part-6-double-checked-locking.html] – towi May 15 '11 at 16:21
  • 2
    @towi - 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. – JohannesD May 15 '11 at 17:22
  • @towi - 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. – JohannesD May 15 '11 at 17:39
  • 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. – Tim Seguine Sep 05 '13 at 13:13
  • @Tim: That's right, the memory barrier is sufficient to cause synchronization with lock/unlock. – Ben Voigt Sep 05 '13 at 13:53
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.

Tim Seguine
  • 2,887
  • 25
  • 38
  • 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... – towi Sep 06 '13 at 07:09
  • @towi 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. – Tim Seguine Sep 06 '13 at 10:08
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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 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? – towi May 15 '11 at 14:02