notesonly.in

One notebook for every subject — open it anywhere.

Log in

Process Synchronization and Semaphores With easy examples.

Process synchronization coordinates processes or threads sharing data, so their interleaved execution stays correct. A semaphore is the classic tool: an integer counter with two atomic operations — wait and signal — that blocks threads until it is safe to proceed.

It matters because unsynchronized sharing corrupts data silently. Two threads incrementing one counter can lose updates; a consumer can read a buffer slot the producer never filled. Semaphores turn these timing accidents into orderly turn-taking.

Key Concepts

  • Critical section: code touching shared data — at most one thread inside at a time.
  • Mutual exclusion: the guarantee that no two threads share the critical section.
  • Binary semaphore (mutex): initialized to 1 — first thread to wait() enters, the rest queue.
  • Counting semaphore: initialized to N — tracks available resources like empty buffer slots.
  • wait(S): S = S − 1; if S goes negative, the thread blocks. signal(S): S = S + 1; if S ≤ 0, one waiter wakes.
  • Producer-consumer: producers fill a bounded buffer, consumers drain it — three semaphores keep it safe. Deadlock: threads waiting on each other forever.

Formulas

  • wait(S): S ← S − 1; block if S becomes negative. signal(S): S ← S + 1; wake one waiter if S ≤ 0.
  • Bounded buffer (capacity n): empty = n, full = 0, mutex = 1. Invariant: empty + full = n, always.
  • Producer: wait(empty) → wait(mutex) → add item → signal(mutex) → signal(full). Consumer: wait(full) → wait(mutex) → remove item → signal(mutex) → signal(empty).

Assumes wait/signal are atomic (the OS guarantees it) and every wait is eventually matched by a signal.

Worked Example

Buffer capacity n = 5. Start: empty = 5, full = 0, mutex = 1. Trace two produces, one consume:

  1. Produce 1: wait(empty): 5 − 1 = 4. signal(full): 0 + 1 = 1. State: empty = 4, full = 1. Check: 4 + 1 = 5 ✓.
  2. Produce 2: wait(empty): 4 − 1 = 3. signal(full): 1 + 1 = 2. State: empty = 3, full = 2. Check: 3 + 2 = 5 ✓.
  3. Consume 1: wait(full): 2 − 1 = 1. signal(empty): 3 + 1 = 4. State: empty = 4, full = 1. Check: 4 + 1 = 5 ✓.
  4. Edge case: four more consumers drive full 1 → 0 → −1 → −2 → −3: three block, waiting for nonexistent items. A later produce raises full −3 → −2 and wakes one.
  5. Why the mutex: counting semaphores track how many slots are free, but only the mutex protects which slot — without it, two producers could grab the same slot.

Common Misconceptions

  • "wait() always waits." The name is historical — it blocks only when the counter goes negative. Think "acquire".
  • "One semaphore suffices for producer-consumer." The mutex alone cannot stop reading an empty buffer or overflowing a full one. You need all three: empty/full for ordering, mutex for exclusion.
  • "Signal order does not matter." Acquiring multiple semaphores in inconsistent orders across threads is the classic deadlock — always acquire in one global order.

Real-World Applications

  • OS kernels: guarding shared data across cores.
  • Connection pools: a counting semaphore capped at pool size makes requests wait instead of overwhelming the database.
  • Printer spoolers: tracking free spool slots; jobs block when full.
  • Embedded systems: interrupt handlers and main loops sharing buffers.

Semaphores distill synchronization to a counter and two operations: wait to claim, signal to release. The producer-consumer trio shows the pattern — counting semaphores enforce "not too many, not too few", the mutex enforces "one at a time" — turning racy buffers into reliable ones.