Process synchronization is a mechanism used in operating systems to manage processes that access shared resources. It helps prevent race conditions and other synchronization problems when multiple processes run at the same time.
In this blog, we will explore process concurrency in operating systems: first understand the problem (race conditions), then learn how synchronization mechanisms solve them step by step.
Think of it like many people cooking in one kitchen: without rules (โonly one person can use the stove at a timeโ), they will bump into each other and ruin the food.

Table of contents
Open Table of contents
Types of processes
Basically, processes can be classified into two types:
- Independent Process: The execution of one process does not affect the others.
- Cooperative Process: A process can affect or be affected by other processes.
graph TB
subgraph Independent["Independent Processes"]
I1["๐ต Music Player"]
I2["๐
Calendar"]
I3["๐งฎ Calculator"]
end
subgraph Cooperative["Cooperative Processes"]
P["๐ญ Producer<br>creates data"] -- "shared buffer" --> S["๐ฆ Buffer<br>(shared resource)"]
S -- "shared buffer" --> C["๐ Consumer<br>reads data"]
end
| Independent | Cooperative | |
|---|---|---|
| Share data? | โ No | โ Yes |
| Affect each other? | โ No | โ Yes |
| Example | Music player & Calculator | Producer & Consumer |
| Need synchronization? | No | Yes |
Only cooperative processes cause concurrency problems, because they share data or resources. That is why we need synchronization.
Race Condition โ The Problem
Before learning how to handle concurrency, we need to understand what can go wrong without it.
What is a Race Condition?
A race condition occurs when multiple threads or processes access shared data concurrently, and the final result depends on the order of execution โ like runners racing to reach the shared data first.
graph LR
A["๐ Process A"] -- "races to write" --> D["๐พ Shared Data"]
B["๐ Process B"] -- "races to write" --> D
D --> R["โ Result depends on<br>WHO arrives first"]
Same input, different execution order โ different results. This unpredictability is the danger.
Scenario: Two withdrawals from one bank account
Suppose two people withdraw money simultaneously from the same bank account ($500). The withdrawal code looks harmless:
// Shared variable โ accessed by BOTH users
int balance = 500;
void withdraw(int amount) {
int current = balance; // โ READ the balance
if (current >= amount) { // โก CHECK enough money?
current = current - amount; // โข COMPUTE new balance
balance = current; // โฃ WRITE back
}
}
Each step alone is fine. But when two processes interleave their steps, disaster happens:
sequenceDiagram
participant A as User A (withdraw $500)
participant M as Memory: balance
participant B as User B (withdraw $500)
Note over M: Initial: $500
A->>M: โ READ balance
M-->>A: $500
B->>M: โ READ balance
M-->>B: $500 โ โ still the OLD value!
Note over A: computes 500โ500 = 0
A->>M: โฃ WRITE balance = $0
Note over B: computes 500โ500 = 0
B->>M: โฃ WRITE balance = $0 โ โ overwrites again!
Note over M: Final: $0 โ but TWO withdrawals happened,<br>$500 was withdrawn TWICE ๐ธ
Step-by-step timeline:
Time User A User B balance
โโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโ
t1 READ โ sees 500 500
t2 READ โ sees 500 500 โ old value
t3 CHECK โ 500 โฅ 500 โ 500
t4 COMPUTE โ 500 โ 500 = 0 500
t5 CHECK โ 500 โฅ 500 โ 500 โ should fail!
t6 WRITE โ set 0 0
t7 COMPUTE โ 500 โ 500 = 0 0
t8 WRITE โ set 0 0 โ double spend!
Result: both withdrawals "succeeded" because User B read the
balance BEFORE User A finished updating it.
TOCTOU Vulnerability
A common form of race condition is TOCTOU (Time-of-Check to Time-of-Use): a program checks the state, then later uses that state, assuming it has not changed in between.
In the example above, the gap between โก CHECK (โbalance โฅ $500โ) and โฃ WRITE is exactly where things break:
โก CHECK: "balance โฅ $500?" โ YES โ (checked at time t3)
โ
โ โ ๏ธ another process changes the balance HERE
โ (between checking and using)
โผ
โฃ USE: "balance = balance โ $500" (used at time t6)
โ The check was valid when made, but meaningless when used.
This pattern appears everywhere:
- Checking file permissions before opening a file
- Checking stock before placing an order
- Checking seat availability before booking
Any โcheck-then-actโ code without locking is vulnerable.
Causes of Race Conditions
1. Simultaneous Access โ two or more processes try to read/write the same shared resource at the same time:
Process A โโโ
โโโโบ ๐ฆ shared resource โ conflict!
Process B โโโ
2. Non-Atomic Operations โ an operation is not atomic when it can be interrupted midway.
- The withdrawal above is really 4 separate steps (
READ โ CHECK โ COMPUTE โ WRITE). - The OS scheduler can pause a process between any two steps to run another process.
3. Lack of Synchronization Mechanisms โ no mechanisms like locks, semaphores, or monitors to control who accesses the resource and when.
Role of Process Synchronization โ The Solutions
Once we understand the problems caused by unsynchronized access to shared resources, we can see why operating systems need process synchronization. Below are the main goals synchronization aims to achieve.
1. Preventing Race Conditions
Ensure that concurrent accesses to shared data never produce inconsistent results, no matter the execution order โ fixing exactly the problem described above.
Now replay the same bank scenario, but this time User B must wait until User A finishes:
Time User A (holds lock ๐) User B (waiting โณ) balance
โโโโ โโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโ
t1 LOCK โ 500
t2 READ โ sees 500 waiting... 500
t3 CHECK โ 500 โฅ 500 โ waiting... 500
t4 WRITE โ set 0 waiting... 0
t5 UNLOCK ๐ LOCK โ 0
t6 READ โ sees 0 0
t7 CHECK โ 0 < 500 โ 0
t8 โ withdrawal rejected โ
0
Result: exactly ONE withdrawal succeeds. Final balance is correct!
Same input, same operations โ but now the result is always correct regardless of scheduling. That is what synchronization guarantees.
2. Mutual Exclusion (Mutex)
Allow only one process inside the critical section at a time.
A critical section is a code segment that accesses shared resources and therefore must be executed by only one process at a time. Every cooperative process follows the same protocol:
flowchart LR
R["๐ Repeat forever"] --> E["๐ช Entry Section<br>request permission (lock)"]
E --> CS["โ ๏ธ Critical Section<br>work with shared resources<br>ONLY ONE process here"]
CS --> X["๐ Exit Section<br>release permission (unlock)"]
X --> RM["๐ด Remainder Section<br>normal code, no shared data"]
do {
lock(&mutex); // ๐ช entry section
/* critical section */ // โ ๏ธ touch shared data here
unlock(&mutex); // ๐ exit section
/* remainder section */
} while (true);
Think of a mutex as a single key to a restroom ๐ป: whoever holds the key gets in; everyone else waits outside until the key is returned.
Banking System example
- Critical Section: Updating an account balance during a deposit or withdrawal.
- Issue without mutex: Two simultaneous transactions could lead to an incorrect balance (see the race condition section).
- With mutex: Transaction B must wait until Transaction A leaves the critical section, then re-read the updated balance.
3. Process Coordination
Sometimes processes need to happen in the right order, not just exclusively. Classic example โ ProducerโConsumer: the consumer must wait if the buffer is empty; the producer must wait if the buffer is full.
stateDiagram-v2
direction LR
s1: Producer produces item
s2: Buffer has space?
s3: Insert item โ signal "data ready"
s4: Consumer waits for "data ready"
s5: Take item โ consume
s1 --> s2
s2 --> s3: yes
s2 --> s2: no โ WAIT โณ
s3 --> s4
s4 --> s5
Without coordination, the consumer might read an empty buffer (garbage data), or the producer might overwrite items nobody consumed yet:
โ Without coordination:
Producer: [A][B][C][D][E] โ buffer FULL, keeps writing โ OVERWRITE โ
Consumer: reads buffer before anything arrives โ reads GARBAGE โ
โ
With coordination:
Producer: writes โ "buffer has data!"
Consumer: hears signal โ reads โ "buffer has space!"
4. Deadlock Prevention
A deadlock happens when two processes each hold a lock and wait forever for the otherโs lock โ a circular wait where nobody can move:
graph LR
P1["๐ Process 1"] -- "holds ๐" --> A["Lock A"]
P2["๐ Process 2"] -- "holds ๐" --> B["Lock B"]
P1 -. "waits foreverโฆ ๐ซ" .-> B
P2 -. "waits foreverโฆ ๐ซ" .-> A
Process 1: lock(A) โ โ lock(B) โฆ blocked (held by Process 2)
Process 2: lock(B) โ โ lock(A) โฆ blocked (held by Process 1)
โ Both stuck FOREVER unless the system intervenes.
Synchronization strategies prevent or break this cycle:
- Lock ordering: all processes must request locks in the same global order (
always A before B) โ circular wait becomes impossible. - Timeouts: if you cannot get a lock within N seconds, release everything and retry.
- Detect & recover: periodically look for cycles in the โwho-waits-for-whomโ graph; kill or roll back one victim process.
Types of Process Synchronization
The two primary types of process synchronization in an operating system are:
| Competitive | Cooperative | |
|---|---|---|
| Relationship | Processes compete for the same shared resource | Processes cooperate by sharing data |
| Analogy | Students fighting for the last slice of pizza ๐ | Two chefs preparing one dish together ๐จโ๐ณ |
| Example | Two print jobs sent to one printer | ProducerโConsumer sharing a buffer |
| Needs | Mutual exclusion (who goes first?) | Correct ordering & handshaking |
graph TB
PS["Process Synchronization"] --> C1["โ๏ธ Competitive<br>compete for resources<br>โ need MUTUAL EXCLUSION"]
PS --> C2["๐ค Cooperative<br>share data & work together<br>โ need COORDINATION"]
In practice, most real systems have both types at the same time โ processes compete for hardware resources while cooperating on shared data.