Skip to main content
Processes, IPC (including semaphores), scheduling, memory, I/O, file systems, virtualization, concurrency models, performance profiling, and the hardware-software interface.

Operating Systems

Processes, IPC (including semaphores), scheduling, memory, I/O, file systems, virtualization, concurrency models, performance profiling, and the hardware-software interface.

Concurrency Visualizer

Race Condition

Step 0 / 0
Speed 100ms
Shared State —
Expected —
Locks Held 0
Status Ready
Playback Paused
Thread Lanes
Shared Memory
—
Mutexes
No locks in this scenario.
Event Timeline
—
Lost Update / Deadlock
No race or deadlock detected.
Step Explanation

Select a scenario and press Play to run the threads.

Wait-For Graph
No wait-for graph.
—
Pseudocode
 

Threads & Concurrency

Intermediate (3/5) ~3–4 hours Threads vs Processes Mutexes Semaphores Race Conditions Deadlock Condition Variables Prereqs: Process Management & Scheduling
Quick Reference

race

No registry entry found for algorithm id "race". If this is a curriculum-only studio, the complexity and quick-reference panel is intentionally omitted.

Threads vs Processes

A process is the OS’s unit of resource ownership: an address space, file descriptors, and a PCB. A thread is the unit of execution: a program counter, a stack, and register state. Threads inside one process share the process’s address space and files — which makes them cheap to create and switch (no address-space swap, no TLB flush) but dangerous to use (shared mutable state).

ProcessThread
Address spacePrivateShared within the process
Failure isolationYes — one process can’t corrupt anotherNo — one thread can crash the process
Context-switch costHigh (page tables, TLB)Low (registers + stack only)
CommunicationIPC (pipes, sockets, shared memory)Direct memory access

This trade-off drives most systems design: processes give isolation (containers, microservices), threads give throughput on shared data (servers, runtimes). A modern server is typically one process, many threads.

User-Level vs Kernel-Level Threads

There are two ways threads can exist:

  • Kernel threads — the OS knows each thread and schedules it; a blocking syscall blocks only that thread. Slightly higher overhead per thread.
  • User-level threads (green threads) — the runtime multiplexes many lightweight threads onto a few kernel threads; switching is very cheap but a blocking call can stall the whole group unless the runtime is careful.

Most production systems are hybrids. Go’s goroutines and Rust’s Tokio are user-level runtimes scheduled on a pool of kernel threads; Java and C++ use kernel threads (with JIT/runtime pools). The practical rule: green threads scale to hundreds of thousands, kernel threads to thousands — with kernel threads giving simpler semantics for blocking I/O.

Race Conditions and Atomicity

The core problem of concurrency: two threads reading and writing the same variable interleave arbitrarily. counter++ is not atomic — it’s a read-modify-write, and two threads can both read the old value and both write the same result, losing an increment.

A race condition is any bug caused by such unsynchronized access. The fix is atomicity: make a critical section indivisible. The OS provides primitives:

  • Mutex (mutual exclusion) — a lock; only one thread holds it at a time.
  • Semaphore — a counter with wait/signal; generalizes locking (mutex ≈ binary semaphore) and enables producer/consumer.
  • Condition variable — lets a thread wait until a condition is true (paired with a mutex); the standard way to block on “queue non-empty.”

Using these correctly is subtle — the discipline is: lock around shared state, unlock on every exit path (RAII/locks in the language), and never hold locks while blocking on user I/O.

Deadlock

Deadlock = every thread waits on a resource held by another, so nothing progresses. The four necessary conditions (Coffman):

  1. Mutual exclusion — resources are non-sharable.
  2. Hold and wait — a thread holds a resource while waiting for another.
  3. No preemption — resources can’t be forcibly taken.
  4. Circular wait — threads form a cycle of waiting.

Break any one and deadlock disappears. Practically, teams avoid circular wait: impose a global lock ordering (always acquire locks A→B→C), use lock acquisition with timeouts where the platform or language supports it (pthread_mutex_timedlock, std::mutex + try_lock_for, .NET Monitor.TryEnter), or prefer message passing (Go channels) to mutexes. Kernels also detect pathological cases by finding cycles in wait-for graphs — the same watchdog/lock-detection mechanism that surfaces as “hung task” or lock-order reports on Linux and other systems.

Condition Variables and Producer/Consumer

The classic pattern: a producer fills a buffer, a consumer drains it. A naive busy loop (while (empty) ;) burns CPU; a condition variable sleeps properly. The same shape appears in every threading model — POSIX pthread_cond_wait, .NET Monitor.Wait, Go channels, and so on:

lock(&m);
while (queue_empty)           // re-check: spurious wakeups
  cond_wait(&cv, &m);         // releases m while waiting
item = dequeue();
unlock(&m);

The while (not if) matters: wait can wake spuriously, and another consumer may have drained the queue. This pattern is the foundation of thread pools, message queues, and the scheduler’s own blocking/waking machinery.

Thread Pools

Creating a thread per request doesn’t scale (allocation + context-switch overhead, unbounded resource use). A thread pool pre-creates N workers that pull work from a shared queue; the queue uses the producer/consumer pattern above. N is typically tuned to the number of cores (CPU-bound) or I/O concurrency (I/O-bound). Almost every server — Java executors, Go’s scheduler, Node’s worker pool, and the runtime behind your web server — is a thread pool.

Thread-Safe Data Structures

Synchronization is a scaling bottleneck, so libraries ship lock-free structures (atomics, CAS, std::atomic, Go channels, Java ConcurrentHashMap) that coordinate with hardware atomics instead of blocking. The trade: lock-free code is fast and deadlock-free but notoriously hard to write correctly — prefer battle-tested libraries over hand-rolled lock-free logic.

Visualizing Races, Locks, and Deadlock

The interactive studio below steps two threads through their instruction streams one instruction at a time. Watch the thread lanes to see the exact interleaving, the shared memory panel to track the counter’s value, and the event timeline for the read-modify-write sequence.

  • Race Condition — both threads read the counter before either writes it back. The second write silently overwrites the first: two increments produce one result. That’s the lost-update race.
  • Mutex-Synchronized — the same increment, but wrapped in lock m / unlock m. T2 blocks on the held lock, waits, then serializes its critical section — the final count is correct.
  • Atomic Increment — counter += 1 as a single hardware read-modify-write. No lock, no waiting, no lost update.
  • Deadlock — the two threads acquire mutexes in opposite order (A then B vs B then A). Each ends up holding one lock and waiting for the other — the wait-for graph shows the circular wait.
  • Lock Ordering Fix — the same code with a consistent A → B acquisition order. No cycle, no deadlock.
  • Producer / Consumer — a bounded buffer of capacity 2. The producer blocks when the buffer fills; the consumer blocks when it empties; each wake is shown on the timeline.

Practice Trajectory

  1. Write a program where 10 threads each increment a shared counter 100,000 times; observe the lost-update race, then fix it with a mutex.
  2. Reproduce deadlock with two mutexes acquired in opposite order across two threads; fix with a global lock ordering.
  3. Build a producer/consumer with a bounded buffer using a condition variable (or Go channels) and verify it blocks instead of busy-waiting.
  4. On a busy server, inspect how many threads a process has and why — top/htop with the threads view (-H) on Linux, Task Manager / Process Explorer on Windows, Activity Monitor on macOS.
  5. Compare a thread-per-request server vs a fixed thread pool under load (e.g. wrk on Unix, or any load generator) and note the throughput difference.

When It’s the Right Tool

SituationTakeaway
Many concurrent operations on shared dataMutexes + condition variables, keep critical sections tiny
Massive concurrency (100k+ tasks)Green threads / async runtimes on a kernel-thread pool
Crash isolation between servicesPrefer processes/containers over shared-memory threads
Debugging hangsThink deadlock (locks, order) before blaming the workload
Production serversThread pools; never thread-per-request at scale