Skip to content

Run-once initialization

Category: Synchronization · Status: stub · Lessons: chapter 04, Waiting for each other (planned)

One line: Running an initializer exactly once however many threads ask for it at the same moment, and handing all of them the same result.

Also called: lazy initialization, double-checked locking, sync.Once, OnceLock, call_once.

How it connects

flowchart LR
  n_once_initialization["Run-once initialization"]
  n_synchronization["Synchronization"]
  n_once_initialization -->|is a| n_synchronization
  classDef center stroke-width:3px
  class n_once_initialization center
  classDef outside stroke-dasharray: 4 3
  class n_synchronization outside

In each language

Rust Once, OnceLock, and LazyLock, a value initialized on first access that can live in a static
Go sync.Once performs exactly one action; sync.OnceValue (Go 1.21) returns a function that calls f only once
C call_once (C11) calls the function exactly once, even from several threads
C++ std::call_once, and a static local variable ↗ is initialized exactly once even when threads reach it concurrently
Java Class initialization runs under a unique per-class initialization lock (JLS §12.4.2 ↗)
Python Not functools.cached_property: since 3.12 it takes no lock, so synchronize the getter yourself
C# Lazy<T> with LazyThreadSafetyMode.ExecutionAndPublication: the initialization is thread-safe, but the object it creates is not protected afterwards
Kotlin by lazy is synchronized by default: the value is computed in one thread and all threads see it
The operating system POSIX pthread_once: later calls with the same once_control do not call the routine again

Where to read more