liveBook · Manning

Double-checked locking pattern

As explained in C++ Concurrency in Action, Second Edition

Double-checked locking is a synchronization pattern that checks a shared pointer before and after acquiring a lock to avoid locking when initialization is complete.

Double-checked locking tries to avoid locking on every access to a lazily initialized resource: it checks the pointer without a lock, then locks and checks again before initializing. The unsynchronized first read can race with initialization, however, causing undefined behavior. section 3.3.1

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
}


!@%STYLE%@!
{"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":2,\"ch\":55},{\"line\":2,\"ch\":57}],[{\"line\":5,\"ch\":55},{\"line\":5,\"ch\":57}],[{\"line\":7,\"ch\":55},{\"line\":7,\"ch\":57}],[{\"line\":10,\"ch\":55},{\"line\":10,\"ch\":57}]]"}
!@%STYLE%@!

The example shows the unlocked pointer check, the locked second check, and the subsequent use of the resource; the first check is the source of the race. section 3.3.1

Use std::once_flag and std::call_once instead to handle one-time initialization safely. section 3.3.1

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Traces both pointer checks and explains how the unsynchronized read causes a data race and undefined behavior; defers detailed memory-model treatment.
Examples
Uses a shared_ptr<some_resource> in a double-checked initialization function and contrasts it with a call_once alternative.
What it assumes
Assumes the shared resource itself can be accessed concurrently once initialized.
What you can do after
Evaluate/understand it by identifying the unsynchronized pointer read as a data race and choosing std::call_once for safe initialization.
How it connects
Connects lazy initialization with mutexes, data races, std::once_flag, and std::call_once.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.