简体   繁体   中英

double checked locking pattern in c++ concurrent programming

I am reading concurrency programming in c++ and came across this piece of code. the book mentioned the potential for nasty race conditions.

void undefined_behaviour_with_double_checked_locking(){

if(!resource_ptr){        //<1>
    std::lock_guard<std::mutex> lk(resource_mutex);
    if(!resource_ptr){        //<2>   
        resource_ptr.reset(new some_resource);        //<3>
    }
}

resource_ptr->do_something();        //<4>

}

here is the quote of explanation from the book. however, i just cant come up with a real example. I wonder if anyone here could help me out.

Unfortunately, this pattern is infamous for a reason: it has the potential for nasty race conditions, because the read outside the lock <1> isn't synchronized with the write done by another thread inside the lock <3>. This therefore creates a race condition that covers not just the pointer itself but also the object pointed to; even if a thread sees the pointer written by another thread, it might not see the newly created instance of some_resource, resulting in the call to do_something() <4> operating on incorrect values.

You don't show what resource_ptr is but from the explanation the reasoning seems to be that "!resource_ptr" (outside the lock) and "resource_ptr.reset" (inside the lock) are not atmoic and are not synchronized with each other.

The use case would be:

  1. thread1 comes into the method, sees that resource_ptr is not populated, enters the lock and is in the middle of the resource_ptr.reset.
  2. thread2 comes into the method and is when checking !resource_ptr may see it as set but resource_ptr may not be fully configured for use.
  3. thread2 falls through to execute "resource_ptr->do_something()" and may see resource_ptr in an inconsistent state and bad things may happen.

I recommend you read this: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf .

Anyway, the gist of it is: the compiler is free to reorder operations as long as they appear to be executed in the program's order in a single threaded situation . On top of that, some CPU architectures take the same liberties with their instruction execution order. So, technically resource_ptr could be modified to point to newly allocated memory before some_resource's constructor has finished. Another thread could at that time see that resource_ptr is not null and attempt to use the not-yet-fully-constructed instance.

The use of a smart pointer instead of a raw pointer might make this less likely, but it doesn't rule it out afaik.

The potential problem is that the write to resource_ptr isn't atomic (inside the reset call). Assuming that resource_ptr is a global or static variable that (/ or otherwise) starts initialized with the value NULL before we get here, it will never cause a thread to fall-through unless the object some_resource is already fully allocated and constructed, however - say that the pointer to this new object is 0x123456789, then it is theoretically possible that resource_ptr has, for example, the value 0x12340000 when another thread does the if (!resource_ptr) test, falls through and uses that value (especially more likely when using aliasing). If resource_ptr is an atomic variable then this code would be fine.

If a program can guarantee that the first time this code is called there is only one thread running (ie, the first call will be from main() before any other thread is created) then this will work fine too, because once initialized, the if test will just always fall through, resulting in only read accesses to resource_ptr while more than one thread is running. In that case you don't need the lock inside the if block though, and you are not allowed to ever write to resource_ptr anywhere else.

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