source

03 Synchronization

Thread Synchronization

The Problem

Since threads share the same virtual-to-physical address mappings and the same address space, we encounter data race problems:

We need mechanisms to ensure threads coordinate properly when accessing shared resources.


Mutexes (Mutual Exclusion)

What is a Mutex?

To support mutual exclusion, the OS provides a construct called a mutex (lock).

Mutex Data Structure

As a data structure, a mutex should have:

Critical Section

The portion of the code protected by the mutex is called the critical section.

The critical section should contain any code that would necessitate restricting access to one thread at a time (executed by one thread at a given moment in time).

Unlock Operations

A mutex is unlocked when:


Condition Variables

What is a Condition Variable?

A condition variable lets threads wait until a certain condition is true.

It works with a mutex:

Data Structure

The condition variable data structure should contain:

Implementation of Wait

// Implementation of wait
Wait(mutex, cond) {
    // Atomically release mutex and place thread on wait queue
    release(mutex);
    add_to(cond.wait_queue, this_thread);
    sleep(this_thread);

    // Later when signaled
    remove_from(cond.wait_queue, this_thread);
    acquire(mutex);
    // Thread resumes execution
}

Readers/Writers Problem

The Problem

We have a shared state where:

The Rules

A naive approach would be wrapping everything in one mutex, but this forces only one thread at a time, even if multiple readers could safely run in parallel (too restrictive).

Solution: Using Counters

Introduce counters:

read_counter = number of readers currently reading
write_counter = 0 or 1

Conditions:

Optimization: Proxy Variable

We can merge into one variable:

resource_counter = 0: resource is free
resource_counter > 0: many readers are active
resource_counter = -1: a writer is active

This is called a proxy variable: it encodes the access state of the resource.

Reader Implementation

// Reader Entry
Lock(counter_mutex) {
    while (resource_counter == -1)
        Wait(counter_mutex, read_phase);  // Wait until safe to read
    resource_counter++;  // Add this reader
} // unlock

// ... read data ...

// Reader Exit
Lock(counter_mutex) {
    resource_counter--;
    if (resource_counter == 0)
        Signal(write_phase);
} // unlock

Writer Implementation

// Writer Entry
Lock(counter_mutex) {
    while (resource_counter != 0)           // Readers or writer active
        Wait(counter_mutex, write_phase);   // Wait until free
    resource_counter = -1;                  // Mark writer active
} // unlock

// ... write data ...

// Writer Exit
Lock(counter_mutex) {
    resource_counter = 0;                   // Resource free again
    Broadcast(read_phase);                  // Wake up all readers
    Signal(write_phase);                    // Wake up one writer
} // unlock

Common Synchronization Issues

Critical Section

A critical section is the part of code where a thread accesses shared state.

It must be protected by appropriate synchronization primitives (mutexes, condition variables, etc.).

Spurious Wake-up

A spurious wake-up happens when:

This is a waste of CPU cycles.

Solution: Always use a while loop instead of if when checking conditions:

while (condition_not_met) {
    Wait(mutex, cond);
}

Deadlocks

What is a Deadlock?

A deadlock happens when two (or more) threads are waiting on each other’s resources, and because of that, none can proceed.

Example

Thread 1:               Thread 2:
Lock(A)                 Lock(B)
Lock(B)  <-- waits      Lock(A)  <-- waits
...                     ...

Both threads are stuck waiting for each other.

Solutions

  1. Release before re-locking: Release all locks before attempting to acquire new ones

  2. Lock all resources upfront: Acquire all necessary locks at the beginning (all-or-nothing approach)

  3. Maintain a lock order (best solution):

    • Define a global ordering of locks
    • All threads must acquire locks in the same order
    • This prevents circular wait conditions

Example of lock ordering:

// Always lock in order: A before B
Lock(A);
Lock(B);
// ... critical section ...
Unlock(B);
Unlock(A);

Kernel vs. User-Level Threads

Kernel-Level Threads

Kernel-level threads are threads that the OS itself manages:

User-Level Threads

User-level threads are managed in user space:


Pthread Synchronization

Pthread is a POSIX standard for threading that provides:

Basic Pthread Mutex Example

pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);

pthread_mutex_lock(&lock);
// Critical section
pthread_mutex_unlock(&lock);

pthread_mutex_destroy(&lock);

Basic Pthread Condition Variable Example

pthread_mutex_t lock;
pthread_cond_t cond;

pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);

// Thread 1 (waiting)
pthread_mutex_lock(&lock);
while (!condition_met) {
    pthread_cond_wait(&cond, &lock);
}
pthread_mutex_unlock(&lock);

// Thread 2 (signaling)
pthread_mutex_lock(&lock);
// ... modify shared state ...
condition_met = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);

Summary