Facebook Pixel

Solutions to Process Synchronization Problems

Getting multiple processes to share resources without stepping on each other is not a simple task. Over time, four main approaches have been developed to tackle this: Locks, Semaphores, Monitors, and Interrupt Disable.

Each one works differently and fits different situations. Understanding when and why to use each mechanism is what separates a working concurrent system from a broken one.

Solutions to Process Synchronization Problems

Overview of the four main synchronization mechanisms

1. Locks

Locks are probably the most straightforward approach to synchronization. The idea is simple: before a process touches a shared resource, it grabs the lock for that resource. If another process already has the lock, the requesting process just waits until it is released. Only then can it proceed.

There are two ways locks are typically built: through software or through hardware.

Software-Based Locks

Software-based locks rely purely on algorithms to manage access. Things like flags, turn variables, or ticket systems are used to decide which process gets to enter the critical section. Some well-known examples include:

  • Peterson's Algorithm: Handles mutual exclusion between two processes using a combination of a turn variable and a flag array.
  • Dekker's Algorithm: A more complex take on the same two-process problem, historically the first known correct solution to mutual exclusion.
  • Bakery Algorithm: Extends mutual exclusion to more than two processes using a token-based numbering system, similar to taking a number at a bakery counter.
  • Test-and-Set Lock: Uses a single atomic check-and-set operation to determine if the lock is free and acquire it in one step.

Hardware-Based Locks

Hardware-based locks use special CPU instructions that are atomic, meaning once they start, they run to completion without anything else interfering. Common hardware instructions include:

  • Test-and-Set (TAS): Atomically reads a memory location and sets it to 1, returning the old value. If the old value was 0, the lock has been acquired.
  • Compare-and-Swap (CAS): Atomically compares a memory location to an expected value and, only if they match, replaces it with a new value.
  • Fetch-and-Add: Atomically increments a value in memory and returns the original value, useful for implementing ticket locks.
The Spinlock Trade-Off
A spinlock is a practical example of a hardware-based lock. The process keeps checking the lock in a tight loop (spinning) until it becomes free. This is efficient when the expected wait time is very short (nanoseconds), but extremely wasteful when the wait is long because the CPU is burning cycles doing absolutely nothing useful while it spins.

2. Semaphores

Semaphores operate at the operating system level rather than the application level. At their core, a semaphore is just a non-negative integer variable that tracks resource availability. It is accessed only through two atomic operations: wait() and signal().

TypeValue RangeUse Case
Binary Semaphore0 or 1Acts like a simple on/off switch for mutual exclusion. Only one process can enter the critical section.
Counting Semaphore0 to NTracks multiple instances of a resource. For example, allowing up to 5 simultaneous database connections.

When a process calls wait(), the semaphore value drops by one. If it goes below zero, the process is blocked and placed in a waiting queue. When another process calls signal(), the value goes up by one and any blocked process gets woken up and given a chance to proceed.

Here is a basic example in C showing how a binary semaphore controls access to a critical section:

c
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>

sem_t semaphore;

void* process(void* arg) {
    sem_wait(&semaphore);    // Acquire (decrement)
    printf("Process %d in critical section\n", *(int*)arg);
    sem_post(&semaphore);    // Release (increment)
    return NULL;
}

int main() {
    pthread_t threads[5];
    int process_ids[5] = {1, 2, 3, 4, 5};
    sem_init(&semaphore, 0, 1);  // Initialize to 1 (binary)

    for (int i = 0; i < 5; i++) {
        pthread_create(&threads[i], NULL, process, &process_ids[i]);
    }
    for (int i = 0; i < 5; i++) {
        pthread_join(threads[i], NULL);
    }
    sem_destroy(&semaphore);
    return 0;
}

Even though 5 threads are created, the binary semaphore ensures that only one thread enters the critical section at a time. Each process enters, prints its ID, exits, and then the next one gets its turn.

3. Monitors

Monitors are a cleaner, higher-level solution compared to semaphores. Instead of manually calling wait and signal everywhere in your code, a monitor packages the shared data and all the operations on it together into one unit. The rule is that only one process can be active inside the monitor at any given time, so mutual exclusion is baked in automatically.

The key advantage over semaphores is that monitors drastically reduce the chance of programmer error. With semaphores, a developer must manually remember to call wait() before entering and signal() after leaving the critical section. Forgetting either one leads to bugs that are extremely difficult to reproduce and debug. With monitors, the locking is handled behind the scenes.

Condition Variables Inside Monitors
Monitors also support condition variables, which let a process pause inside the monitor and wait until some specific condition becomes true before it continues. A classic example is a producer-consumer system: the consumer calls wait() on a condition variable when the buffer is empty, and the producer calls signal() on that same variable after adding data to the buffer.

4. Interrupt Disable

This is the most low-level approach of the four. When a process needs to enter its critical section, it simply turns off all hardware interrupts on the processor. Since the CPU cannot be interrupted, no context switch can occur, which means no other process can run. Once the process is done with its critical section, it turns interrupts back on.

The obvious and fatal downside is that this only works on single-processor systems. On a multi-processor machine, disabling interrupts on one CPU does absolutely nothing to stop another CPU from accessing the same shared resource at the same time. This makes interrupt disabling largely useless in modern multi-core systems.

Even on a single-processor system, keeping interrupts disabled for too long is dangerous because the system cannot respond to critical hardware events like disk I/O completions or network packets arriving.

Choosing the Right Mechanism

MechanismLevelBest ForKey Limitation
Software LocksApplicationSimple mutual exclusion between a small number of processesComplex to implement correctly, prone to subtle bugs
Hardware LocksApplication/KernelShort critical sections where spinning is acceptableWastes CPU cycles during long waits (busy waiting)
SemaphoresOS LevelControlling access to resource pools with known capacityEasy to misuse; forgetting signal() causes deadlocks
MonitorsOS/Language LevelComplex synchronization with automatic lockingNot natively supported in all programming languages
Interrupt DisableHardware/KernelVery short kernel-level critical sections on uniprocessorsCompletely ineffective on multi-processor systems

Knowing which mechanism to reach for is part of what makes designing concurrent systems a skill rather than just a checklist. Locks are common at the application level, semaphores and monitors handle things at the OS level, and interrupt disable is reserved for very specific low-level scenarios where a single processor is involved.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.