Thread: Double-Check Locking in C++

  1. #1
    Registered User
    Join Date
    Dec 2011
    Posts
    5

    Lightbulb Double-Check Locking in C++

    Hi

    I read in wiki, that the double-check pattern can have negative side effects (depending on the language/hardware combination) and could thus be an anti-pattern. I just introduced it in our framework, but for C++ I could not yet reproduce this bad behavior.

    Consider this C++-style code:
    Code:
    if(global_ptr == 0){
       mutex->Lock();
       if(global_ptr == 0)
          global_ptr = new Object();
       mutex->Unlock();
    }
    The problem, as mentioned in wiki, is that one thread could still create the object and has not completed his work during another thread, passing the first if-clause, could expect an already fully created object and goes ahead with a pointer to an incomplete created object.

    I tried to reproduce it with pthreads and some time-delay in the constructor. But I always received first the "ready" status (object gives out after creation) and then the pointers are returned.

    So, is it an anti-pattern in C++ or not? How can I correct it if so?

    Thanks

  2. #2
    Registered User
    Join Date
    Sep 2008
    Posts
    200
    Code:
    if(global_ptr == 0){
       mutex->Lock();
       if(global_ptr == 0)
          global_ptr = new Object();
       mutex->Unlock();
    }
    There is no harm in this - because global_ptr is both checked and set whilst holding the mutex lock, it will not be created twice, so long as all other threads are cooperating in the same way. The wiki either has it wrong, or is talking about the case where you don't do the second check, in which case another thread may have grabbed the mutex before you do, but after you check global_ptr == 0, and thus also think it has the responsibility of creating the object. Like I said, the important thing is that it is both tested and set whilst holding the mutex.

    Its only purpose (that I can see) is to save yourself having to acquire the mutex every time you do this check, which may cause performance issues if you have a lot of threads using it a lot (and thus also checking it a lot), and thus a lot of threads having to get the mutex.

    However, you should note that pthreads has a pthread_once() function for exactly the case where a function must be run exactly once for all threads for just this situation, since it is quite common - you might want to take a look at it.


    I tried to reproduce it with pthreads and some time-delay in the constructor. But I always received first the "ready" status (object gives out after creation) and then the pointers are returned.
    Be wary of such tests for this sort of thing - threading is unpredictable, and a problem you don't ever see can rear its ugly head on another user's computer for absolutely no reason whatsoever.
    Last edited by JohnGraham; 12-08-2011 at 04:58 AM.

  3. #3
    Registered User
    Join Date
    Jun 2005
    Posts
    6,815
    Quote Originally Posted by JohnGraham View Post
    There is no harm in this
    ... except for the fact it is not guaranteed to work.

    It's ability to work in C++ depends on the memory model of the processor, reorderings performed by the compiler, and details of how the compiler makes use of the underlying synchronisation functions.

    None of these are specified in the C++ standard (only the last is sort of specified in C++-11, IIRC) so the compiler is free to do as it likes.

    There has been a whole bunch of articles written on this premature optimisation, and why it is not guaranteed to work.
    Right 98% of the time, and don't care about the other 3%.

    If I seem grumpy or unhelpful in reply to you, or tell you you need to demonstrate more effort before you can expect help, it is likely you deserve it. Suck it up, Buttercup, and read this, this, and this before posting again.

  4. #4
    Registered User
    Join Date
    Sep 2008
    Posts
    200
    Quote Originally Posted by grumpy View Post
    It's ability to work in C++ depends on the memory model of the processor, reorderings performed by the compiler, and details of how the compiler makes use of the underlying synchronisation functions.
    Ah yes, I am of course assuming that global_ptr has properly been declared volatile, so the compiler isn't allowed to optimize accesses that occur across sequence points.

  5. #5
    Registered User
    Join Date
    Dec 2011
    Posts
    5
    Ok, I see.

    Means, that it works on my PC could be an exception.

    So far the object is first be created and than afterwards assigned.

    And volatile makes it work then?

    Thanks for the evaluation.

  6. #6
    Registered User
    Join Date
    Sep 2008
    Posts
    200
    To clarify about making global_ptr volatile: You have to make the pointer volatile - i.e. this:

    Code:
    Object * volatile global_ptr;
    which qualifies the variable, and not this:

    Code:
    volatile Object *global_ptr;
    which qualifies the pointed-to Object, allowing accesses to global_ptr to still be cached across sequence points.

    But again, pthread_once() may be a preferable option.

  7. #7
    Registered User Codeplug's Avatar
    Join Date
    Mar 2003
    Posts
    4,981
    >> I am of course assuming that global_ptr has properly been declared volatile
    volatile has nothing to do with multi-threading programming - and DCL is still broken with it.

    http://www.aristeia.com/Papers/DDJ_J...04_revised.pdf

    gg

  8. #8

  9. #9
    Registered User
    Join Date
    Sep 2008
    Posts
    200
    Quote Originally Posted by Codeplug View Post
    volatile... DCL is still broken with it.

    http://www.aristeia.com/Papers/DDJ_J...04_revised.pdf
    I stand corrected. Very good article.


    Quote Originally Posted by Codeplug View Post
    volatile has nothing to do with multi-threading programming
    Now getting me to believe that will take quite some convincing... but please try. I have neglected to use volatile on a flag in a multi-threaded program before (at least once) and because the compiler could see that the current thread never changed its value, it assumed it never changed, took its initial value as permanent, leading to bad. I add volatile, it's forced to re-read changes made outside its scope of knowledge (i.e. from another thread) and my program works because it can see the state of affairs has changed. So in this case, I find it pretty clear that volatile = useful in multi-threaded programming... or am I missing something?
    Last edited by JohnGraham; 12-08-2011 at 07:03 PM.

  10. #10
    Officially An Architect brewbuck's Avatar
    Join Date
    Mar 2007
    Location
    Portland, OR
    Posts
    7,396
    Quote Originally Posted by grumpy View Post
    ... except for the fact it is not guaranteed to work.

    It's ability to work in C++ depends on the memory model of the processor, reorderings performed by the compiler, and details of how the compiler makes use of the underlying synchronisation functions.

    None of these are specified in the C++ standard (only the last is sort of specified in C++-11, IIRC) so the compiler is free to do as it likes.

    There has been a whole bunch of articles written on this premature optimisation, and why it is not guaranteed to work.
    Any mutex that works properly will use acquire/release semantics with appropriate fencing. All stores to memory, in particular storing the pointer to the object, will be performed before the lock is unlocked.
    Code:
    //try
    //{
    	if (a) do { f( b); } while(1);
    	else   do { f(!b); } while(1);
    //}

  11. #11
    Registered User Codeplug's Avatar
    Join Date
    Mar 2003
    Posts
    4,981
    >> I add volatile, ...
    The compiler has no control on hardware reodering that may occur. More info here: MSDN volatile sample

    gg

  12. #12
    Registered User
    Join Date
    Dec 2011
    Posts
    5
    Quote Originally Posted by Codeplug View Post
    >> I am of course assuming that global_ptr has properly been declared volatile
    volatile has nothing to do with multi-threading programming - and DCL is still broken with it.

    http://www.aristeia.com/Papers/DDJ_J...04_revised.pdf

    gg
    Really nice paper. Probably one knows how this situation is improved under the new standard, as there is thread support now and they should have a new memory model. See: Foundations of the C++ Concurrency Memory Model, Hans-J. Boehm (2008)

    So the memory model changed, but is it already somewhere implemented? Is it improving anything due to Double-Checking?

  13. #13
    Registered User Codeplug's Avatar
    Join Date
    Mar 2003
    Posts
    4,981
    Under C++11 you can just do:
    Code:
    Singleton& Instance()
    {
        static Singleton s;
        return s;
    }
    This is thread-safe under C++11. Hopefully, the compiler will generate efficient thread-safe code for it. Here are other things you can do in C++11: http://www.justsoftwaresolutions.co....d-locking.html

    gg

  14. #14
    Officially An Architect brewbuck's Avatar
    Join Date
    Mar 2007
    Location
    Portland, OR
    Posts
    7,396
    Quote Originally Posted by Sanches View Post
    Really nice paper. Probably one knows how this situation is improved under the new standard, as there is thread support now and they should have a new memory model. See: Foundations of the C++ Concurrency Memory Model, Hans-J. Boehm (2008)

    So the memory model changed, but is it already somewhere implemented? Is it improving anything due to Double-Checking?
    Don't yet Codeplug make you paranoid. There is no big debate about volatile or memory reordering, at least among people with a clue. I think Codeplug is just trying to cover all the bases.

    There's no chance in hell the store to memory will happen after the mutex is unlocked, because the mutex surely uses release semantics when unlocking the synchronization object. If it didn't, it wouldn't be much of a mutex.
    Code:
    //try
    //{
    	if (a) do { f( b); } while(1);
    	else   do { f(!b); } while(1);
    //}

  15. #15
    Cat without Hat CornedBee's Avatar
    Join Date
    Apr 2003
    Posts
    8,895
    Quote Originally Posted by brewbuck View Post
    There's no chance in hell the store to memory will happen after the mutex is unlocked, because the mutex surely uses release semantics when unlocking the synchronization object. If it didn't, it wouldn't be much of a mutex.
    A release doesn't do anything without an acquire to synchronize with, and the thing about DCL is that the second thread never acquires the mutex, and thus never synchronizes with the release.
    All the buzzt!
    CornedBee

    "There is not now, nor has there ever been, nor will there ever be, any programming language in which it is the least bit difficult to write bad code."
    - Flon's Law

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Double check
    By calc in forum C Programming
    Replies: 2
    Last Post: 07-11-2009, 03:25 AM
  2. need someone to double check this for me (c)..
    By flamehead144 in forum C Programming
    Replies: 2
    Last Post: 02-16-2009, 12:19 PM
  3. Double-Checked Locking pattern issue
    By George2 in forum C++ Programming
    Replies: 3
    Last Post: 01-02-2008, 04:29 AM
  4. can someone double check and maybe improve my code
    By tommy69 in forum C Programming
    Replies: 23
    Last Post: 04-21-2004, 02:04 PM
  5. Double check on Java code.
    By sean in forum A Brief History of Cprogramming.com
    Replies: 0
    Last Post: 07-05-2002, 09:55 AM

Tags for this Thread