This post introduces the process synchronization problem in operating systems.
Background: Critical-Section Problem
When multiple processes execute concurrently, each of them has a segment of code (the critical section) that accesses and modifies shared resources. Without a synchronization mechanism, multiple processes may enter their critical sections at the same time and operate on the shared resources, leading to data inconsistency and race conditions.
Requirements for a solution:
- Mutual exclusion
- Progress: If no process is executing in its critical section and there exist processes waiting to enter, the selection of entering processes cannot be postponed indefinitely
- Bounded waiting
Synchronization Mechanisms
Peterson’s Solution
Assume that load and store instructions are atomic and cannot be interrupted. The two processes share two variables: int turn indicates whose turn it is to enter the critical section, and the array boolean flag[2] indicates whether a process is ready to enter its critical section; flag[i] = true means process 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int turn; // which process's turn it is to enter the critical section
boolean flag[2]; // whether each process is ready to enter the critical section
void P0()
{
flag[0] = true; // process P0 is ready to enter the critical section
turn = 1; // it is process P1's turn to enter the critical section
while (flag[1] && turn == 1); // if process P1 is ready to enter the critical section and it is P1's turn, process P0 waits
critical section // process P0 enters the critical section
flag[0] = false; // process P0 exits the critical section
}
void P1()
{
flag[1] = true; // process P1 is ready to enter the critical section
turn = 0; // it is process P0's turn to enter the critical section
while (flag[0] && turn == 0); // if process P0 is ready to enter the critical section and it is P0's turn, process P1 waits
critical section // process P1 enters the critical section
flag[1] = false; // process P1 exits the critical section
}
Limitation:
To improve performance, modern processors and/or compilers may reorder read and write operations that have no dependencies.
When instruction reordering happens, instructions that were meant to execute sequentially may have their execution order changed, causing both processes to enter their critical sections at the same time.
Hardware Support for Synchronization
Memory barriers
A memory barrier is an instruction that forces any change in memory to be propagated (visible) to all other processors.
When a memory barrier instruction is executed, within the same process it ensures that all preceding load and store operations complete before any subsequent load / store operations are performed. Even if the processor or compiler reorders instructions, the memory barrier guarantees that the store operations are completed in memory and are visible to other processors before any future load / store operations.
Hardware Instructions
Atomic variable: the execution of one or more instructions is indivisible, either all of it executes or none of it does.
Limitation: unbounded waiting, the order in which competing processes acquire the resource is random.
Test-and-Set instruction
1 | bool TestAndSet(bool *target) |
Example:1
2
3
4
5
6
7
8bool lock = false;
void P0()
{
while (TestAndSet(&lock)); // if lock is true, wait
critical section // process P0 enters the critical section
lock = false; // process P0 exits the critical section
}
From the perspective of other concurrently executing threads or processes, it completes instantaneously: either it executes in full or it does not execute at all.
Compare-and-Swap instruction
1 | int compare_and_swap(int *value, int expected, int new_value) |
bounded waiting version:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18while (true)
{
/* entry section – acquire the lock */
waiting[i] = true;
key = 1;
while (waiting[i] && key == 1) // use key to exit
key = compare_and_swap(&lock, 0, 1);
waiting[i] = false;
/* critical section – release one process */
j = (i + 1) % n;
while ((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i)
lock = 0;
else
waiting[j] = false;
/* remainder section */
}
Atomic Variables
1 | void increment(atomic_int *v) |
Mutex Locks and Semaphores
Mutex Locks
The mutex lock is the simplest synchronization tool. A process must acquire the lock before entering its critical section and must release the lock when it leaves. A mutex lock contains a boolean variable available that indicates whether the lock is available:1
2
3
4
5
6
7
8
9
10
11
12
13typedef struct
{
bool available;
} mutex;
void acquire(mutex *m) {
while (!m->available); // spin (busy) waiting
m->available = false; // acquire the lock
}
void release(mutex *m) {
m->available = true; // release the lock
}
Usage example:1
2
3
4
5
6mutex lock; // available is initially true
// ...
acquire(&lock);
critical section // critical section
release(&lock);
remainder section
Main characteristics:
- Mutual exclusion: at any moment only one process can hold the lock
- Busy waiting: a process keeps occupying the CPU while waiting for the lock
- Suitable for:
- Cases where the expected waiting time is short
- Multiprocessor systems
Drawbacks:
- Busy waiting wastes CPU time
- Priority inversion may occur: when a low-priority process holds the lock, a high-priority process has to wait
Semaphores
A semaphore is a more powerful synchronization tool, proposed by Dijkstra. A semaphore S is an integer variable that, apart from initialization, can only be accessed through two atomic operations, wait (P) and signal (V):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27typedef struct
{
int value; // value of the semaphore
struct process *list; // waiting queue
} semaphore;
void wait(semaphore *S)
{ // P operation
S->value--;
if (S->value < 0)
{
// add the current process to the waiting queue
add_to_list(S->list);
block(); // block the current process
}
}
void signal(semaphore *S)
{ // V operation
S->value++;
if (S->value <= 0 && S->list != NULL)
{
// wake up one process from the waiting queue
process *p = remove_from_list(S->list);
wakeup(p);
}
}
Types of semaphores:
Binary Semaphore
- The value can only be 0 or 1
- Similar to a mutex lock, but implemented differently
- Used for mutual exclusion and synchronization
Counting Semaphore
- The value can be any integer
- Used for resource management
- For example: controlling access to a finite set of resources
Usage example:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// Mutual exclusion example
semaphore mutex = 1; // initialized to 11
wait(&mutex);
critical section
signal(&mutex);
// Producer-consumer problem example
semaphore empty = n; // initialized to the buffer size
semaphore full = 0; // initialized to 00
semaphore mutex = 1; // for mutually exclusive access
// Producer
wait(&empty); // wait for an empty slot
wait(&mutex); // enter the critical section
// add data to the buffer
signal(&mutex); // leave the critical section
signal(&full); // one more full slot
// Consumer
wait(&full); // wait for a full slot
wait(&mutex); // enter the critical section
// remove data from the buffer
signal(&mutex); // leave the critical section
signal(&empty); // one more empty slot
Advantages of semaphores:
- No busy waiting; a blocked process consumes no CPU
- Can implement more complex synchronization
- Applicable to many synchronization scenarios
Disadvantages of semaphores:
- Misuse easily leads to deadlock
- Correctness is hard to guarantee
- Easy to make programming mistakes (wrong order of wait/signal)
Monitors
Abstraction: encapsulate the shared data together with the procedures that operate on it, providing a mechanism that ensures only one process can access the shared data at any given time.1
2
3
4
5
6
7
8
9
10
11monitor monitor_name {
// shared variable declarations
// monitor initialization code
// procedures that operate on the shared variables
procedure P1(...) { ... }
procedure P2(...) { ... }
...
procedure Pn(...) { ... }
}

Synchronization Problem Formulations
Bounded-Buffer Problem
1 | monitor BoundedBuffer |
1 | BoundedBuffer bb; // create a monitor instance |
Readers-Writers Problem
1 | monitor ReaderWriter |
1 | ReaderWriter rw; // create a monitor instance |
Dining-Philosophers Problem
1 | monitor DiningPhilosophers |
1 | DiningPhilosophers dp; // create a monitor instance |
Translated from the Chinese original.