andersch.dev

<2025-04-20 Sun>

Synchronization Primitives

Synchronization primitives are mechanisms that coordinate concurrent execution between multiple threads to avoid race conditions and ensure data consistency.

Examples:

Mutex

In multithreading, a mutex (mutual exclusion) is a synchronization primitive that prevents multiple threads from simultaneously accessing shared resources.

It acts as a lock that only one thread can hold at a time - when a thread acquires the mutex, others attempting to access the same resource must wait until the mutex is released

The use of mutexes can prevent race conditions, but may also introduce performance bottlenecks and deadlocks.

Basic Mutex API:

int mutex_init(mutex_t *mutex);
int mutex_lock(mutex_t *mutex);    // blocking, waits until mutex available
int mutex_trylock(mutex_t *mutex); // non-blocking, returns with success or failure
int mutex_unlock(mutex_t *mutex);  // unlock a mutex
int mutex_destroy(mutex_t *mutex);

Spinlock

A spinlock is a synchronization primitive used in multithreading where a thread repeatedly checks a condition until it becomes available ("spinning").

Spinlocks cause CPU load (unlike mutexes) and block execution by actively spinning. However, they avoid the overhead of context switching, which makes them suitable for for brief waits or very short critical sections.

Code Example

typedef volatile int spinlock_t;

void spin_lock(spinlock_t *lock) {
    while (__sync_lock_test_and_set(lock, 1)) {
        // Spin until lock is acquired
    }
}

void spin_unlock(spinlock_t *lock) {
    __sync_lock_release(lock);
}

Semaphore

Semaphores are synchronization primitives used to control access to shared resources in concurrent programming. They maintain a counter that represents the number of available resources.

Main types:

  • Binary semaphores: Counter values are 0 or 1 (functioning like mutexes)
  • Counting semaphores: Counter can have arbitrary non-negative values

Core operations:

  • wait/P/down: Decrement counter if it's > 0, otherwise blocks until it can
  • signal/V/up: Increment counter, potentially unblocking waiting processes

Basic Semaphore API:

typedef struct {
    int value;
    /* ... */
} semaphore;

void sem_init(semaphore *sem, int value);
void sem_wait(semaphore *sem);
int  sem_trywait(semaphore *sem); // non-blocking
void sem_post(semaphore *sem);
int  sem_value(semaphore *sem, int *sval);
void sem_destroy(semaphore *sem);

Condition Variable

Condition variables are synchronization primitives used in multithreading that allow threads to wait until a condition is met. During this time, threads are suspended (sleeping), which avoids busy waiting (spinning).

They are typically used alongside mutexes to solve the producer-consumer problem and similar synchronization scenarios.

Usage:

  • Thread atomically releases a mutex and enters wait state
  • Waiting thread awakens by signal (one thread) or broadcast (all threads)
  • Upon waking, thread reacquires mutex before proceeding

Basic Condition Variable API

int cond_init(cond_t **cond);
int cond_wait(cond_t *cond, mutex_t *mutex);
int cond_timedwait(cond_t *cond, mutex_t *mutex, uint64_t timeout_ms);
int cond_signal(cond_t *cond);    // awaken single waiting thread
int cond_broadcast(cond_t *cond); // awaken all waiting threads
int cond_destroy(cond_t *cond);