Back to Tutorials
Last edited September 4, 2026
September 4, 2026
8 min read
Intermediate
Computer Systems
Interactive visualizer

Processes, Threads and Concurrency Visualized

See how processes isolate resources, threads share state, schedules interleave, and synchronization prevents races while creating new tradeoffs.

Concurrency Trace Visualizer

Loading visualizer...

Concurrency becomes difficult because multiple execution flows can observe and modify shared state in different valid orders. The individual instructions may be simple. The number of possible interleavings is not.

This tutorial uses deterministic schedules so a particular failure can be inspected step by step. An operating-system scheduler is more complex, and real compilers, processors, and runtimes may transform or reorder work under rules specific to their memory models. The traces are reasoning tools, not literal universal instruction streams.

What a process represents

A process is a running program together with an execution environment managed by the operating system. Conceptually it has an identity, an isolated virtual address space, executable code, and resources such as open files, communication endpoints, and security credentials. The details differ across operating systems, but the boundary is useful: one ordinary process does not directly read and write another process's address space.

Isolation limits accidental corruption and helps the OS enforce permissions. A crashing application usually should not overwrite the memory of an unrelated one. Isolation is not absolute protection against every bug or vulnerability, but it is a foundational containment boundary.

Separate processes still need to cooperate. Inter-process communication can take forms such as pipes, sockets, shared-memory regions, files, or OS-specific message facilities. The important point is that communication crosses an explicit boundary. Shared memory can be fast, but it reintroduces synchronization questions because more than one process can access the same region.

Threads share a process

A thread is an execution flow within a process. Threads conceptually share the process's address space and resources, so they may refer to the same heap objects, globals, files, and sockets. Each thread also needs its own execution state: an instruction position, register state, and call stack containing its active function frames.

That combination is powerful and dangerous. Sharing makes communication cheap: one thread can update an object another thread can read. It also means an uncoordinated update can violate the assumptions of every thread using that object.

Two terms need a clean boundary:

  • Concurrency means multiple tasks make progress during overlapping periods.
  • Parallelism means multiple tasks physically execute at the same time.

One CPU core can support concurrency by alternating between tasks. Several cores can execute threads in parallel. A concurrent design therefore does not imply parallel hardware, and parallel execution is only one possible schedule for concurrent tasks.

Scheduling and context switching

An OS scheduler chooses which eligible thread runs. A simplified state model is:

  • runnable: able to execute when scheduled;
  • running: currently executing on a processor;
  • blocked or waiting: unable to proceed until an event, resource, or timeout occurs.

A context switch saves enough state for one execution flow and restores another. Application code can create work, block, yield through runtime facilities, or express priorities where supported, but ordinary code does not directly dictate every scheduler decision.

This uncertainty is central to concurrent correctness. A program must remain correct across all schedules its synchronization permits, not only the schedule seen during one test run.

A lost update

Start with a shared counter equal to 5. Thread A and Thread B each execute:

TypeScript
counter += 1;

At source level that looks like one operation. For teaching, decompose it into read, modify, and write:

TEXT
Thread A                    Thread B
read 5
                            read 5
compute 6
                            compute 6
write 6
                            write 6

Two increments should produce 7, but this valid interleaving produces 6. Both threads read the same old value, calculate the same replacement, and the second write overwrites rather than builds on the first. This is a lost update.

A race condition exists when correctness depends on which competing operation wins or on an uncontrolled ordering between operations. It can disappear under logging, debugging, different hardware, or a quieter workload because those changes influence timing without repairing the missing coordination.

The read-modify-write decomposition is educational. Actual machine instructions, compiler transformations, runtime representations, and visibility rules can be more involved. The invariant remains: an increment is unsafe if other execution flows can conflict with its intermediate state and no appropriate synchronization makes the whole transition indivisible.

Shared state and critical sections

Shared mutable state can be observed or changed by more than one execution flow. A critical section is the region whose operations must satisfy a coordination rule to preserve an invariant. For the counter, the invariant is that every accepted increment contributes exactly one to the final value.

Mutual exclusion allows at most one participant into a protected section at a time. It converts many possible interleavings inside that region into a serialized order. The final order may still vary—A then B or B then A—but both orders preserve the counter invariant.

Good critical sections are intentionally scoped. Protecting too little leaves a race. Protecting unrelated slow work increases contention. Protecting blocking I/O can make every waiter inherit that latency.

Mutexes and locks

A mutex expresses ownership around a critical section:

TEXT
lock
  -> read / modify / write shared state
unlock

If A owns the mutex, B waits before entering. A can read 5 and write 6; after A releases, B reads 6 and writes 7. The visualizer shows both ownership and waiting because a lock is not free. Contended work becomes serialized, waiting adds latency, and a long-held lock can reduce throughput.

Locks also do not make code automatically correct. Every conflicting access must follow a compatible synchronization policy. The protected region must include the whole invariant-changing operation. Exceptions and early returns must not accidentally skip release; language facilities that tie lock lifetime to scope help.

Atomic operations

An atomic operation appears indivisible with respect to competing operations covered by the same atomic guarantees. An atomic increment can combine the conceptual read-modify-write into one coordinated update, so another atomic increment cannot observe and overwrite its intermediate value in the same way.

Atomics can avoid a mutex for carefully scoped state such as counters or flags, but they do not automatically preserve invariants spanning several variables. Visibility and ordering between memory operations are a deeper topic, and languages expose different memory models. Use the documented primitive rather than assuming that a normal source-level assignment is atomic enough.

Deadlock is not a race

Consider two locks:

TEXT
Thread 1: holds Lock A, waits for Lock B
Thread 2: holds Lock B, waits for Lock A

Neither thread can release the lock it holds because it is waiting to acquire the other. This circular wait produces deadlock: no progress. A race produces an ordering-dependent incorrect outcome; a deadlock produces permanent waiting. A program can have either or both.

Common mitigations include acquiring multiple locks in one consistent global order, minimizing nested locking, using try-lock or timeouts where failure and retry semantics are appropriate, and avoiding unnecessary shared mutable state. None is universally sufficient. Consistent ordering only helps when every participant follows it; timeouts recover control but do not make a partially completed operation correct.

Other concurrency failures

Starvation means a participant repeatedly fails to obtain enough execution or access to a resource. The system may progress while one thread does not. Livelock means participants keep reacting to one another but still make no useful progress, like two people continually stepping aside in the same direction. Visibility and ordering issues occur when one thread cannot assume another thread's writes become observable in source-code order without the guarantees supplied by the language and synchronization primitive.

These are distinct failure modes. Naming the actual one leads to a better fix than treating every concurrency defect as a race.

Where the model appears in software

Servers handle overlapping requests and often coordinate connection pools, caches, or session state. UI applications keep interaction responsive while background work performs I/O, but UI state usually has thread-affinity rules. Worker pools bound concurrency rather than creating an unbounded thread per task. Games and real-time servers coordinate shared world state. Operating systems continuously schedule runnable work.

Database transactions solve a related coordination problem at a different layer: concurrent operations must preserve data invariants despite overlapping execution and failures. A database lock is not interchangeable with a language mutex, but both force the engineer to define who may observe or change shared state and when.

Design before synchronization

The easiest shared state to synchronize is state that is not shared. Immutable values, message passing, ownership transfer, partitioned data, and per-request state can remove entire classes of interleavings. When sharing is necessary, write the invariant first, then choose the narrowest primitive whose documented guarantees preserve it.

Test deterministic schedules for known edge cases, but do not confuse a passing stress test with proof. The scheduler owes application code no convenient ordering. Correct concurrent code makes the allowed ordering explicit.