user1747505
user1747505

Reputation: 23

Acquire barrier in the double checked locking pattern

In C++ and the Perils of Double-Checked Locking, the authors give an example on how to implement the pattern correctly.

Singleton* Singleton::instance () {
   Singleton* tmp = pInstance;
   ... // insert memory barrier (1)
   if (tmp == 0) {
      Lock lock;
      tmp = pInstance;
      if (tmp == 0) {
         tmp = new Singleton;
         ... // insert memory barrier (2)
         pInstance = tmp;
      }
   }
   return tmp;
}

What I couldn't figure out, though, is if the first memory barrier must be after Singleton* tmp = pInstance;? (EDIT: To be clear, I understand that the barrier is needed. What I don't understand is if it must come after assigning tmp) If so why? Is the following not valid?

Singleton* Singleton::instance () {
   ... // insert memory barrier (1)
   if (pInstance == 0) {
      Lock lock;
      if (pInstance == 0) {
         Singleton* tmp = new Singleton;
         ... // insert memory barrier (2)
         pInstance = tmp;
      }
   }
   return pInstance;
}

Upvotes: 2

Views: 462

Answers (2)

SergeyA
SergeyA

Reputation: 62573

Why are you still talking about the paper from 2004? C++ 11 guarantees static variables are initialized only once. Here is your fullly-working, 100% correct singleton (which, of course, is an anti-pattern on it's own):

static TheTon& TheTon::instance() {
    static TheTon ton;
    return ton;
}

Upvotes: 1

David Schwartz
David Schwartz

Reputation: 182761

It is essential. Otherwise, reads that occur after the if may be prefetched by the CPU before the copy, which would be a disaster. In the case where pInstance is not NULL and we don't acquire any locks, you must guarantee that reads that occur after the read of pInstance in the code are not re-ordered to before the read of pInstance.

Consider:

Singleton* tmp = pInstance;
if (tmp == 0) { ... }
return tmp->foo;

What happens if the CPU reads tmp->foo before tmp? For example, the CPU could optimize this to:

bool loaded = false;
int return_value = 0;

if (pInstance != NULL)
{ // do the fetch early
     return_value = pInstance->foo;
     loaded = true;
}

Singleton* tmp = pInstance;
if (tmp == 0) { ... }

return loaded ? return_value : tmp->foo;

Notice what this does? The read of tmp->foo has now moved to before the check if the pointer is non-NULL. This is a perfectly legal memory prefetch optimization (speculative read) that a CPU might do. But it's absolutely disastrous to the logic of double checked locking.

It is absolutely vital that code after the if (tmp == 0) not prefetch anything from before we see pInstance as non-NULL. So you need something to prevent the CPU from reorganizing the code's memory operations as above. A memory barrier does this.

Upvotes: 2

Related Questions