简体   繁体   中英

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

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM