1

I just read the excellent blog C++ and the Perils of Double-Checked Locking

And I don't understand why we have to use the first memory barrier in Example 12 (as below):

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

Is it safe to change it to code below? Why not?

Singleton* Singleton::instance () {
       if (pInstance == 0) {
          Lock lock;
          if (pInstance == 0) {
             Singleton* tmp = new Singleton;
             ... // insert memory barrier
             pInstance = tmp;
          }
       }
       return pInstance;
    }
ricky
  • 2,058
  • 4
  • 23
  • 49

1 Answers1

0

No, it isn't safe. Reading the three paragraphs before the example, and the two after it, the potential problem is a system where the write to pInstance is done (flushed to memory) on thread B before the construction of Singleton has been flushed. Then thread A could read pInstance, see the pointer as non-null, and return it potentially allowing thread A to access the Singleton before thread B has finished storing it into memory.

The first flush is necessary to ensure that the flushing of the writes during construction of Singleton have been completed before you try to use it in a different thread.

Depending on the hardware you're running on this might not be a problem.

1201ProgramAlarm
  • 32,384
  • 7
  • 42
  • 56
  • isn’t the second barrier guarantee that pInstance always stored after tmp’s construction is done? – ricky Oct 25 '18 at 04:21
  • It does for thread B, but the hardware cache could flush out the update to `pInstance` before all of the writes during Singleton construction have been flushed to main memory. Then thread A (running on a different CPU) would see the update value of `pInstance` but not the fully constructed Singleton. This is what the paragraph 3 above the example mentions. – 1201ProgramAlarm Oct 25 '18 at 04:25
  • so the second barrier makes sure that the execution (in thread X) is in right order as it's written (construct, then assign to `pInstance`), not reordered by compiler. and the first barrier makes sure that another thread (Y) will see the value in the same order as it assigned in thread X? – ricky Oct 25 '18 at 04:49
  • Yes, by ensuring that all previous writes by thread X can be seen by thread Y. – 1201ProgramAlarm Oct 25 '18 at 04:54
  • If I add a memory barrier as the first statement of my second `Singleton::instance ()` function, then is it become safe now? – ricky Oct 25 '18 at 04:58
  • No. If thread X gets past the memory barrier, thread Y writes to `pInstance`, then thread X reads `pInstance` you still have a potential problem. This is why the original code reads `pInstance` into `tmp` before the barrier. – 1201ProgramAlarm Oct 25 '18 at 05:13