pthread_once() to run initialization code exactly once in multithreaded programs1. The Problem: Initialization That Must Happen Exactly Once
Many programs and library functions need to perform some one-time initialization before they can work correctly. For example, a library function might need to:
- Initialize a mutex with special attributes using
pthread_mutex_init() - Allocate a global data structure that all threads will use
- Create a Thread-Specific Data key using
pthread_key_create() - Open a connection or load a resource needed by all threads
In a single-threaded program, this is easy: just do it before anything else runs. In a library function that will be called from multiple threads, it is harder:
- The library function doesn’t control when or from how many threads it is called
- The main program may create threads before the first library call
- Multiple threads may call the library function at the same time — including the very first call
- The initialization must run once, not once per thread, not multiple times
2. Why a Simple Boolean Flag Doesn’t Work
A beginner’s instinct is to use a global boolean flag to track whether initialization has been done:
/* ===== WRONG: Race condition on the flag itself ===== */
static int initialized = 0; /* Global flag */
static SomeResource *resource; /* Global resource to initialize */
void library_function(void)
{
if (!initialized) { /* Thread A and Thread B may BOTH see 0 here */
resource = create_resource(); /* Both threads initialize — DOUBLE INIT! */
initialized = 1;
}
use_resource(resource);
}
initialized and sees 0. Before Thread A can call create_resource(), the OS schedules Thread B. Thread B also checks initialized and also sees 0. Now both threads call create_resource() — double initialization! Even adding a check after setting the flag is not safe without proper synchronization.3. The Solution: pthread_once()
The Pthreads API provides pthread_once() specifically to solve this problem. It guarantees that a given initialization function is called exactly once, regardless of how many threads call pthread_once() with the same control variable, and regardless of whether they call it simultaneously.
int pthread_once(pthread_once_t *once_control, void (*init)(void));
Returns: 0 on success, positive error number on error
Parameters explained:
| Parameter | Type | Description |
|---|---|---|
once_control |
pthread_once_t * |
Pointer to a control variable that tracks whether init has been called. Must be statically initialized to PTHREAD_ONCE_INIT. |
init |
void (*)(void) |
Pointer to the initialization function to call. Takes no arguments and returns nothing. |
The pthread_once_t Control Variable
The pthread_once_t type is an opaque data type used internally by the Pthreads library to track the state of a one-time initialization. You must:
- Declare it as a global or static variable (so all threads see the same variable)
- Initialize it to
PTHREAD_ONCE_INITat declaration time - Never modify it yourself — only pass it to
pthread_once()
/* Required declaration pattern — always static/global, always PTHREAD_ONCE_INIT */
static pthread_once_t once_var = PTHREAD_ONCE_INIT;
The first time pthread_once() is called with a pointer to this variable, it calls the init function and modifies the control variable internally to record that initialization is done. All subsequent calls — from any thread — see the “already done” state and skip the init call.
4. How pthread_once() Works Internally
pthread_once() with the same once_var, the init function is called exactly once. The Pthreads implementation handles all the synchronization internally.5. The Initialization Function
The function pointed to by init must have this signature:
void init_function(void)
{
/* Perform one-time initialization here */
/* No arguments, no return value */
}
The function runs with no arguments. If you need to pass data to it, you must use global variables. This is acceptable here because the whole point is to set up global (or process-wide) resources that all threads will share.
6. Complete Code Examples
/* ===== EXAMPLE 1: Basic pthread_once() Usage ===== */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
/* Control variable — MUST be static/global, MUST be initialized to PTHREAD_ONCE_INIT */
static pthread_once_t once_control = PTHREAD_ONCE_INIT;
/* Shared resource that gets initialized only once */
static int *shared_resource = NULL;
/* Initialization function — called exactly once, no matter how many threads */
static void initialize_resource(void)
{
printf("Initializing shared resource (this prints only ONCE)\n");
shared_resource = malloc(sizeof(int) * 100);
if (shared_resource == NULL) {
fprintf(stderr, "malloc failed\n");
exit(EXIT_FAILURE);
}
shared_resource[0] = 42; /* Initialize with some value */
}
/* Library function that many threads may call */
void use_shared_resource(int thread_id)
{
int s;
/* Ensure initialization happens exactly once, even if called from many threads */
s = pthread_once(&once_control, initialize_resource);
if (s != 0) {
fprintf(stderr, "pthread_once failed: %d\n", s);
return;
}
/* At this point, shared_resource is guaranteed to be initialized */
printf("Thread %d: shared_resource[0] = %d\n", thread_id, shared_resource[0]);
}
/* Thread function */
static void *thread_func(void *arg)
{
int thread_id = *(int *)arg;
use_shared_resource(thread_id);
return NULL;
}
int main(void)
{
pthread_t threads[5];
int ids[5];
int i;
/* All 5 threads may call use_shared_resource() simultaneously.
initialize_resource() will be called exactly ONCE. */
for (i = 0; i < 5; i++) {
ids[i] = i + 1;
pthread_create(&threads[i], NULL, thread_func, &ids[i]);
}
for (i = 0; i < 5; i++)
pthread_join(threads[i], NULL);
free(shared_resource);
return 0;
}
/* Expected output (order may vary, but "Initializing..." appears exactly once):
Initializing shared resource (this prints only ONCE)
Thread 1: shared_resource[0] = 42
Thread 3: shared_resource[0] = 42
Thread 2: shared_resource[0] = 42
... */
/* ===== EXAMPLE 2: pthread_once() for Mutex with Special Attributes ===== */
/* In early Pthreads, you couldn't statically initialize a mutex with special
attributes (like PTHREAD_MUTEX_ERRORCHECK or PTHREAD_MUTEX_RECURSIVE).
pthread_once() was the standard solution. */
#include <stdio.h>
#include <pthread.h>
static pthread_once_t mutex_once = PTHREAD_ONCE_INIT;
static pthread_mutex_t my_mutex; /* Can't use PTHREAD_MUTEX_INITIALIZER
if we need special attributes */
static void init_mutex(void)
{
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
/* Set error-checking type — this requires pthread_mutex_init(), not static init */
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&my_mutex, &attr); /* Initialize with attributes */
pthread_mutexattr_destroy(&attr);
printf("Mutex initialized with ERRORCHECK attribute\n");
}
void thread_safe_operation(void)
{
/* Ensure mutex is initialized exactly once */
pthread_once(&mutex_once, init_mutex);
pthread_mutex_lock(&my_mutex);
/* ... critical section ... */
pthread_mutex_unlock(&my_mutex);
}
/* NOTE: Today, for simple cases, PTHREAD_MUTEX_INITIALIZER works fine.
pthread_once() is still useful when the init function does more complex work. */
7. Why pthread_once() Exists — Historical Context
In early versions of the Pthreads specification, it was not possible to statically initialize a mutex. Every mutex had to be initialized dynamically using pthread_mutex_init(). This meant library functions had a chicken-and-egg problem: they needed a mutex to safely initialize their resources, but initializing the mutex itself required a mutex.
pthread_once() was introduced specifically to solve this problem. It provides a way to run any initialization code exactly once without needing a pre-existing mutex.
Later, statically initialized mutexes (PTHREAD_MUTEX_INITIALIZER) were added to the standard, which reduced the need for pthread_once() in some cases. However, pthread_once() is retained because:
- It is more convenient than managing a boolean flag plus a mutex
- It handles more complex initialization scenarios elegantly
- It is particularly useful when combined with Thread-Specific Data (as we’ll see in File 3)
Alternative: Static Mutex + Static Boolean
For comparison, here is how you would do the same thing manually — it is more verbose and error-prone:
/* Manual alternative to pthread_once() — more verbose, same effect */
static pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER;
static int already_initialized = 0;
void maybe_initialize(void)
{
pthread_mutex_lock(&init_mutex);
if (!already_initialized) {
do_initialization(); /* Run once */
already_initialized = 1;
}
pthread_mutex_unlock(&init_mutex);
}
/* pthread_once() is simply the standardized, cleaner version of this pattern. */
Interview Questions
pthread_once()?pthread_once() ensures that a specified initialization function is called exactly once, no matter how many threads call pthread_once() with the same control variable. It is used when a library function needs to perform one-time setup (like creating a mutex or allocating a shared resource) without knowing which thread will call it first or how many threads will call it simultaneously.
PTHREAD_ONCE_INIT and why is it required?PTHREAD_ONCE_INIT is a macro that gives a pthread_once_t variable its initial state — indicating that the initialization function has not yet been called. Every pthread_once_t variable must be initialized to this value before being passed to pthread_once(). Without this initialization, the behavior is undefined because the Pthreads library uses the variable’s internal state to track what has happened.
pthread_once() with the same control variable for the first time?The Pthreads library ensures that the initialization function is called by exactly one thread. The other thread blocks (waits) until the initialization function completes. Once the first thread finishes the initialization and pthread_once() returns, the waiting thread also returns — but without calling the initialization function again. This is the key thread-safety guarantee of pthread_once().
pthread_once_t variable be declared as static or global?It must be static or global because all threads that will call pthread_once() must share the same control variable. If it were a local variable, each thread would have its own copy, and the library would have no way to tell which thread already ran the initialization. The shared variable is what allows the library to synchronize across threads.
pthread_once() historically important? Is it still needed today?Historically, early Pthreads implementations did not support static mutex initialization. Since any thread-safe initialization needed a mutex, there was a chicken-and-egg problem. pthread_once() solved this by providing a synchronization mechanism that did not require a pre-initialized mutex. Today, statically initialized mutexes (PTHREAD_MUTEX_INITIALIZER) exist, making pthread_once() less critical for basic mutex setup. However, it is still useful for any initialization that is more complex than just a mutex — especially creating Thread-Specific Data keys.
pthread_once() for per-thread initialization? Why or why not?No. pthread_once() is designed for process-wide, one-time initialization — the init function runs once for the entire process, not once per thread. For per-thread initialization (running some code the first time each thread calls a function), you would use Thread-Specific Data: check pthread_getspecific() for NULL, and if NULL, do the per-thread setup.
