Synchronisation
3.1 The Critical Section Problem
Section titled “3.1 The Critical Section Problem”Consider processes sharing a resource. The critical section is the code segment accessing the Resource. A correct solution must satisfy:
- Mutual exclusion: At most one process executes in its critical section at any time.
- Progress: If no process is in its critical section and some wish to enter, only those not in their remainder section participate, and the decision cannot be postponed indefinitely.
- Bounded waiting: A bound exists on the number of times other processes enter their critical sections after a process has requested entry.
3.2 Hardware Support
Section titled “3.2 Hardware Support”Test-and-Set. Atomic read-and-set instruction:
boolean test_and_set(boolean *target) { boolean rv = *target; *target = true; return rv;}Compare-and-Swap (CAS). Atomically compare and conditionally write:
boolean compare_and_swap(int *value, int expected, int new_value) { int temp = *value; if (*value == expected) { *value = new_value; } return temp == expected;}These instructions are the foundation for lock-free and wait-free data structures.
3.3 Mutexes
Section titled “3.3 Mutexes”A mutex provides exclusive access to a shared resource.
pthread_mutex_t lock;pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);// critical sectionpthread_mutex_unlock(&lock);pthread_mutex_destroy(&lock);Spinlocks. A mutex that busy-waits. Useful when the expected wait time is shorter than the Context-switch overhead.
void spinlock_acquire(volatile int *lock) { while (__atomic_test_and_set(lock, __ATOMIC_ACQUIRE)) { // busy wait }}
void spinlock_release(volatile int *lock) { __atomic_clear(lock, __ATOMIC_RELEASE);}3.4 Semaphores
Section titled “3.4 Semaphores”A semaphore is an integer variable accessed only through two atomic operations:
- Wait (P): : while do no-op; .
- Signal (V): : .
Counting semaphore: takes any non-negative integer. Controls access to a resource with Multiple instances.
Binary semaphore: . Functionally similar to a mutex, but can be signalled by any Process (not just the owner).
sem_t mutex;sem_init(&mutex, 0, 1);
sem_wait(&mutex);// critical sectionsem_post(&mutex);Theorem 3.1. Semaphores can implement mutual exclusion correctly if initialised to 1 and used With wait on entry and signal on exit.
Proof. If the semaphore is 1, the first wait decrements it to 0 and enters. Any concurrent wait finds and blocks. When the first process signals, becomes 1, waking exactly one Blocked process.
3.5 Monitors and Condition Variables
Section titled “3.5 Monitors and Condition Variables”A monitor is a high-level synchronisation construct encapsulating shared data and operations, Allowing only one process to execute within it at any time.
A condition variable allows a process to wait for a condition inside a monitor:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;int buffer_count = 0;
// Producerpthread_mutex_lock(&mutex);while (buffer_count == BUFFER_SIZE) { pthread_cond_wait(&cond, &mutex);}buffer_count++;pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);
// Consumerpthread_mutex_lock(&mutex);while (buffer_count == 0) { pthread_cond_wait(&cond, &mutex);}buffer_count--;pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);pthread_cond_wait atomically releases the mutex and blocks. On wakeup, it re-acquires the mutex.
3.6 Classic Synchronisation Problems
Section titled “3.6 Classic Synchronisation Problems”Producer-Consumer (Bounded Buffer). A producer writes items to a shared buffer of size ; a Consumer reads them.
sem_t empty, full, mutex;sem_init(&empty, 0, N);sem_init(&full, 0, 0);sem_init(&mutex, 0, 1);
void producer() { while (true) { item = produce_item(); sem_wait(&empty); sem_wait(&mutex); insert_item(item); sem_post(&mutex); sem_post(&full); }}
void consumer() { while (true) { sem_wait(&full); sem_wait(&mutex); item = remove_item(); sem_post(&mutex); sem_post(&empty); consume_item(item); }}Readers-Writers. Multiple readers access concurrently; writers require exclusive access.
sem_t rw_mutex, mutex;int read_count = 0;sem_init(&rw_mutex, 0, 1);sem_init(&mutex, 0, 1);
void writer() { sem_wait(&rw_mutex); // write to shared data sem_post(&rw_mutex);}
void reader() { sem_wait(&mutex); read_count++; if (read_count == 1) sem_wait(&rw_mutex); sem_post(&mutex); // read shared data sem_wait(&mutex); read_count--; if (read_count == 0) sem_post(&rw_mutex); sem_post(&mutex);}Dining Philosophers. Five philosophers, five forks between them. Each needs two forks to eat. Naive solution (one semaphore per fork) deadlocks when all pick up their left fork.
Solutions:
- Resource hierarchy: Pick up lower-numbered fork first.
- Arbitrator: A single semaphore limits concurrent eaters to four.
- Chandy-Misra: Forks are “owned” by a philosopher; others request via messages.
sem_t forks[5];sem_t room;for (int i = 0; i < 5; i++) sem_init(&forks[i], 0, 1);sem_init(&room, 0, 4);
void philosopher(int i) { while (true) { think(); sem_wait(&room); sem_wait(&forks[i]); sem_wait(&forks[(i + 1) % 5]); eat(); sem_post(&forks[(i + 1) % 5]); sem_post(&forks[i]); sem_post(&room); }}:::caution Common Pitfall Always use a while loop (not if) when checking conditions with condition variables. Spurious Wakeups can cause pthread_cond_wait() to return without the condition being signalled. The loop Re-checks the condition after every wakeup.
:::