How std::call_once Guarantees Thread Safety in C++
This article explains how the C++ standard library mechanism
std::call_once provides thread-safe, single-execution
guarantees across concurrent environments. In conjunction with
std::once_flag, std::call_once prevents race
conditions during initialization by synchronizing calling threads,
managing internal state flags, handling exceptions properly, and
enforcing strict memory visibility rules without requiring developers to
manually configure mutexes.
The Role of
std::once_flag
The guarantee of single execution relies on a shared state maintained
inside an instance of std::once_flag. The
std::once_flag object acts as a state machine rather than a
simple boolean flag. Internally, it tracks three distinct states:
- Uninitialized: The function has never run.
- In Progress: A thread is currently executing the specified function.
- Completed: The function has executed successfully and returned.
Every invocation of std::call_once takes a reference to
this flag, reading and updating the internal state atomically.
Synchronization and Thread Blocking
When multiple threads concurrently invoke std::call_once
with the same std::once_flag:
- Election of the Active Thread: The first thread to query the flag transitions its state from "uninitialized" to "in progress" using an atomic operation. This thread is selected to execute the callable target.
- Blocking Competing Threads: Any other thread
reaching
std::call_oncewhile the state is "in progress" does not execute the function. Instead, these threads are blocked (typically suspended using OS-level synchronization primitives like futexes on Linux or wait addresses on Windows) until the active thread finishes. - Early Exit for Subsequent Callers: If a thread
arrives after the state has reached "completed",
std::call_onceperforms an acquire-load on the flag and returns immediately without running the function or acquiring locks.
Memory Ordering Guarantees
std::call_once establishes a strict
synchronizes-with relationship between the thread performing
the initialization and all threads that subsequently observe the
completion.
- The completing thread executes a release operation upon finishing the callable.
- All threads returning from
std::call_onceperform an acquire operation.
This memory barrier guarantees that all memory writes, object
constructions, or resource allocations performed inside the callable are
fully visible to every thread after std::call_once
returns.
Exception Safety and Retries
The execution is only marked as "completed" if the callable returns normally. If an uncaught exception escapes the callable:
- The exception is propagated to the calling thread.
- The state within
std::once_flagis reset from "in progress" back to "uninitialized". - The next thread waiting in the queue is unblocked and promoted to execute the callable again.
Execution continues to be attempted until one invocation successfully completes without throwing an exception.