How absl::call_once Ensures Thread-Safe One-Time Initialization in C++

absl::call_once implements a lock-free state machine using a 32-bit atomic control word that transitions through four distinct states—uninitialized, running, waiting, and done—to guarantee a callable executes exactly once across concurrent threads.

The Abseil C++ library provides absl::call_once in [absl/base/call_once.h](https://github.com/abseil/abseil-cpp/blob/master/absl/base/call_once.h) as a high-performance alternative to std::call_once, eliminating lock contention through a carefully designed wait-free fast path and an efficient spin-lock fallback mechanism.

The Atomic State Machine Behind absl::call_once

At the core of absl::call_once lies a state machine encoded in a single std::atomic<uint32_t> control word. This design allows multiple threads to coordinate initialization without allocating mutexes or kernel objects.

The once_flag Control Word

Each absl::once_flag embeds a std::atomic<uint32_t> control_ member initialized to kOnceInit (value 0). The flag is non-copyable and non-movable, ensuring that the control word's address remains stable for the program's lifetime. Because the initial state is zero, global or static once_flag objects require no runtime initialization.

Four States of Initialization

The control word cycles through four distinct values representing the lifecycle of the one-time operation:

State Value Description
kOnceInit 0 Initialization has not started
kOnceRunning 0x65C2937B A thread is currently executing the user function
kOnceWaiter 0x05A308D2 One or more threads are waiting for completion
kOnceDone 221 The function completed successfully

The unusual hex constants for kOnceRunning and kOnceWaiter serve as debugging aids while keeping the initial state zero-initialized.

The Lock-Free Algorithm Step-by-Step

The implementation in absl/base/call_once.h uses a hybrid approach: a lock-free fast path for the common case, followed by a spin-lock protocol for contended scenarios.

Fast-Path with Acquire Semantics

When absl::call_once is invoked, it first loads the control word using memory_order_acquire. If the value equals kOnceDone, the function returns immediately without synchronization overhead. This makes repeated calls to call_once essentially free after initialization completes.

// Simplified conceptual flow
if (flag.control_.load(std::memory_order_acquire) == kOnceDone) {
  return;  // Already initialized
}

CAS Transition to Running State

If the fast-path fails, the thread attempts to atomically replace kOnceInit with kOnceRunning using compare_exchange_strong. The thread that succeeds becomes the owner and proceeds to execute the user callable. All other threads fail the CAS and enter the waiting protocol.

uint32_t expected = kOnceInit;
if (flag.control_.compare_exchange_strong(expected, kOnceRunning,
                                          std::memory_order_acq_rel)) {
  // This thread is the owner; run the initialization
} else {
  // Another thread owns initialization; wait
}

Spin-Lock Waiting for Completion

Threads that fail to acquire the running state invoke base_internal::SpinLockWait, defined in [absl/base/internal/spinlock_wait.h](https://github.com/abseil/abseil-cpp/blob/master/absl/base/internal/spinlock_wait.h). This routine uses a transition table to determine behavior based on the current state:

static const base_internal::SpinLockWaitTransition trans[] = {
    {kOnceInit,    kOnceRunning, true},
    {kOnceRunning, kOnceWaiter,  false},
    {kOnceDone,    kOnceDone,    true}
};

The wait logic operates as follows:

  • If the word is kOnceInit: The thread retries the CAS (another thread may have completed between the initial check and the wait).
  • If the word is kOnceRunning: The thread atomically changes the state to kOnceWaiter and parks.
  • When the owner finishes: It writes kOnceDone with release semantics and wakes waiters via SpinLockWake.

Exception Safety and Scheduling Considerations

Beyond the core state machine, absl::call_once handles edge cases critical to robust C++ applications.

Exception Handling Behavior

If the user-provided callable throws an exception, the control word remains in the kOnceRunning state. This design ensures that subsequent calls to call_once will retry the initialization, preventing a failed initialization from permanently blocking the system. Only successful completion stores kOnceDone.

Cooperative Scheduling Support

absl::call_once supports cooperative multitasking environments through SchedulingHelper. When invoked with SCHEDULE_KERNEL_ONLY mode, the implementation temporarily disables kernel-only rescheduling to prevent deadlocks in cooperative scheduler contexts. This logic resides in [absl/base/internal/low_level_scheduling.h](https://github.com/abseil/abseil-cpp/blob/master/absl/base/internal/low_level_scheduling.h).

Low-Level Variants and Implementation Location

For internal scheduler initialization where standard scheduling modes might recurse, Abseil provides absl::base_internal::LowLevelCallOnce. This variant forces SCHEDULE_KERNEL_ONLY mode but otherwise implements the identical state machine algorithm.

All public APIs and implementations reside in the header file:

Practical Usage Example

The following example demonstrates thread-safe singleton initialization using absl::call_once:

#include "absl/base/call_once.h"
#include <iostream>
#include <thread>

absl::once_flag init_flag;

void Initialize() {
  // Expensive one-time work (e.g., initializing a singleton)
  std::cout << "Running initialization on thread "
            << std::this_thread::get_id() << '\n';
}

// Each thread calls this; the body runs only once.
void Worker() {
  absl::call_once(init_flag, Initialize);
  std::cout << "Worker thread continues: " << std::this_thread::get_id()
            << '\n';
}

int main() {
  std::thread t1(Worker);
  std::thread t2(Worker);
  std::thread t3(Worker);
  t1.join(); t2.join(); t3.join();
}

Output (order may vary, but “Running initialization …” appears exactly once):


Running initialization on thread 140735578455040
Worker thread continues: 140735578455040
Worker thread continues: 140735570062336
Worker thread continues: 140735561669632

For rare cases requiring early scheduler initialization, use the low-level variant:

absl::once_flag low_flag;
absl::base_internal::LowLevelCallOnce(&low_flag, []{
    // Initialization that must run before any kernel-only scheduling.
});

Summary

  • absl::call_once uses a lock-free atomic state machine in absl/base/call_once.h to guarantee single execution.
  • The four-state protocol (kOnceInit, kOnceRunning, kOnceWaiter, kOnceDone) coordinates between owner and waiting threads without kernel mutexes.
  • Fast-path optimization provides zero-cost overhead for subsequent calls after initialization completes.
  • Exception safety leaves the flag in a retryable state if the callable throws, preventing permanent initialization failure.
  • Cooperative scheduling support via SchedulingHelper ensures correct behavior in kernel-only and hybrid scheduling environments.

Frequently Asked Questions

What happens if the callable passed to absl::call_once throws an exception?

If the user callable throws, the control word remains in the kOnceRunning state according to the implementation in absl/base/call_once.h. This causes subsequent calls to retry the initialization, ensuring the system never assumes completion when an exception occurred. Only a successful return stores kOnceDone.

How is absl::call_once different from std::call_once?

absl::call_once uses a custom lock-free state machine with a spin-wait fallback, while std::call_once typically relies on implementation-defined mutexes or condition variables. The Abseil version provides a fast-path that returns immediately with memory_order_acquire if initialization is complete, and it supports cooperative scheduling modes that standard library implementations may not handle.

Why does absl::call_once use such unusual hexadecimal values for its states?

The constants 0x65C2937B for kOnceRunning and 0x05A308D2 for kOnceWaiter serve as debugging aids. These distinctive values make it easy to identify the current state when inspecting memory or debugging core dumps, while keeping kOnceInit as zero allows for zero-initialization of global once_flag objects.

Can I use absl::call_once with cooperative scheduling environments?

Yes. The implementation includes SchedulingHelper to handle cooperative scheduling modes defined in absl/base/internal/scheduling_mode.h. When the mode is SCHEDULE_KERNEL_ONLY, absl::call_once temporarily disables kernel-only rescheduling to prevent deadlocks. For initialization that must run before the scheduler is fully initialized, use absl::base_internal::LowLevelCallOnce.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →