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:

  1. Uninitialized: The function has never run.
  2. In Progress: A thread is currently executing the specified function.
  3. 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:

  1. 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.
  2. Blocking Competing Threads: Any other thread reaching std::call_once while 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.
  3. Early Exit for Subsequent Callers: If a thread arrives after the state has reached "completed", std::call_once performs 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_once perform 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_flag is 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.