Skip to content
Phuc's Blog
Go back

Process Synchronization in Operating Systems: A Developer-Friendly Guide

Edit page

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.

Note

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.

Process Concurrency

Table of contents

Open Table of contents

Types of processes

Basically, processes can be classified into two types:

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
IndependentCooperative
Share data?โŒ Noโœ… Yes
Affect each other?โŒ Noโœ… Yes
ExampleMusic player & CalculatorProducer & Consumer
Need synchronization?NoYes

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"]
Note

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:

Warning

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.

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)

Note

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

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:


Types of Process Synchronization

The two primary types of process synchronization in an operating system are:

CompetitiveCooperative
RelationshipProcesses compete for the same shared resourceProcesses cooperate by sharing data
AnalogyStudents fighting for the last slice of pizza ๐Ÿ•Two chefs preparing one dish together ๐Ÿ‘จโ€๐Ÿณ
ExampleTwo print jobs sent to one printerProducerโ€“Consumer sharing a buffer
NeedsMutual 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"]
Note

In practice, most real systems have both types at the same time โ€” processes compete for hardware resources while cooperating on shared data.


Edit page
Share this post:

Previous Post
Sensors and Actuators in Internet of Things (IoT)
Next Post
Transaction Concurrency Control in Database Systems

Comments