1st Principles

Systems · 32 min read

Concurrency and Processes

Why a server must handle many clients at once, how processes, threads, and async overlap conceptually, and how race conditions appear when shared state meets overlapping work.

Why this exists

Client-server designs concentrate many clients on listening services. Those services cannot help only one client at a time if the product expects overlapping users.

Concurrency is how software structures overlapping work. Processes, threads, and asynchronous event loops are different mechanisms with shared themes: scheduling, isolation, and shared-state risk.

Without this vocabulary, "the server is slow," "it deadlocked," and "two updates clobbered each other" stay mysterious. This lesson connects those failures to the client-server need to serve many callers.

Axioms & primitives

  1. 01Concurrency means overlapping progress on more than one task; parallelism means running those tasks at the same instant on multiple hardware workers.
  2. 02A process is an OS-isolated program instance with its own memory address space (in the usual model).
  3. 03Threads share a process address space and can run concurrent work with less isolation and cheaper context switches than separate processes.
  4. 04Async and event-loop designs overlap waiting (I/O) without one OS thread per client.
  5. 05Shared mutable state under concurrency needs a policy: locks, queues, immutability, or single-owner rules.
  6. 06A race condition is a bug that depends on timing or interleaving; it is a concurrency failure mode, not a network protocol.

Learning objectives

After this lesson you should be able to:

  • Explain why a single-threaded blocking "one client at a time" server fails the usual client-server product expectation.
  • Contrast process isolation with thread shared memory in one paragraph each.
  • Describe what an async event loop overlaps that a blocking thread-per-request model spends waiting on.
  • Name a lost update on a shared counter as a race and propose one mitigation class.
  • Separate "many connections" from "many CPUs" when reading a performance symptom.

Progressive depth

Read the layers in order for a full explanation. Or open the layer you need.

01

Intuition

A server that listens (from the client-server lesson) will receive overlapping requests. If it fully finishes client A's slow database read before it even accepts client B, B waits for A's entire story. Products that serve many users need overlapping work.

Three common shapes appear in real systems. Multi-process: run several OS processes behind a load balancer; each process may still be simple inside. Multi-thread: one process hosts many threads that share memory and serve requests together. Async/event-driven: mostly one (or few) threads switch between tasks whenever a task would wait on I/O.

The hard part is rarely "can I accept a socket." The hard part is shared state: counters, caches, session maps, in-memory shopping carts. When two overlapping tasks read-modify-write the same value without a rule, you get a race: lost updates, double charges, or torn reads.

Concurrency is not the same as "use more machines." You can be concurrent on one CPU. Parallelism is when hardware truly runs steps at the same time. Both matter. They answer different questions.

02

Formal shape

Process: the OS unit of isolation. Separate address spaces make accidental memory corruption across programs harder. Communication uses explicit channels (sockets, pipes, files, shared-memory regions you opt into). Crash of one process need not crash another.

Thread: a schedulable execution context inside a process. Threads share heaps and globals. Coordination uses locks, atomics, concurrent data structures, or message queues. Deadlock is mutual waiting for locks. Starvation is never getting scheduled or never getting the lock.

Async / event loop: tasks are state machines or coroutines. When a task would block on I/O, it yields. The loop runs another ready task. This overlaps waiting well. CPU-heavy work still blocks the loop unless offloaded to workers. "Async" is a scheduling style, not automatic safety for shared data.

Race condition: correctness depends on interleaving. Classic lost update: both tasks read count=10, both write count=11, one increment vanishes. Mitigations include mutual exclusion (locks), atomic read-modify-write, compare-and-swap loops, single-writer ownership, queues that serialize mutations, and moving the truth to a datastore with transactions (see state and persistence).

Server models map to client-server load: thread-per-request, process-per-request (or prefork), and async multiplexers are different answers to "many clients, one service." Load balancers add another process boundary in front.

03

Worked examples

Example A: blocking toy server. Accept connection, read request, query DB (2 seconds), write response, then accept the next client. Under ten concurrent clients, queueing latency explodes even if the CPU is idle during DB waits.

Example B: thread pool. Each request borrows a worker thread. Overlap improves. If the pool has 50 threads and 5,000 waiting clients, acceptance still queues. Pool size is a capacity knob, not infinity.

Example C: async HTTP server. Thousands of open connections mostly wait on clients or downstream APIs. The loop shines when work is I/O bound. A single tight JSON parse loop of 200ms stalls everyone on that worker.

Example D: race. Two requests increment an in-memory "likes" counter without synchronization. Under load, the counter drifts low. Fix classes: atomic increment, lock around the update, or store likes in a database with an atomic UPDATE likes = likes + 1.

Example E: process isolation for safety. A browser site isolation story and a multi-process app server both use process boundaries so one crash or compromise is less likely to take all memory with it. Cost: more RAM and more IPC.

04

Edge cases

The GIL and language runtimes: some runtimes limit CPU parallelism of threads for interpreter reasons while still allowing I/O overlap. Read your runtime's real model before assuming multi-core speedups.

False sharing and cache-line contention make "more threads" regress on hot counters. Performance work needs measurement.

Tail latency: average looks fine while the 99th percentile request hits lock convoys or GC pauses. Concurrency bugs often show up as rare spikes.

Distributed concurrency: two server replicas are concurrent processes without shared memory. Races move to the database or cache. Soft state in memory is not shared across replicas unless you design it.

Security angle: data races can become security bugs when checks and uses interleave (TOCTOU). Authorization checks need the same careful sequencing as counters.

Over-synchronization: one giant global lock removes races and destroys overlap. Prefer smaller critical sections or ownership designs.

Mental models

  • Restaurant kitchen

    One cook who finishes an entire order before starting the next is a blocking server. Several cooks, or one cook who starts the oven then plates another dish while waiting, are concurrency strategies.

  • Separate offices vs shared desk

    Processes are separate offices with locked doors (isolation). Threads are coworkers at one desk sharing drawers (shared memory). Misplaced papers are easier at the shared desk.

  • Post office queue

    An event loop is a clerk who never naps during mail wait time: start a request, park it when waiting on the network, serve the next window customer, resume when the reply arrives.

  • Two editors, one document

    Without turn-taking rules, the last save wins and work vanishes. That is a race on shared state.

Common misconceptions

  • Myth

    More threads always make the server faster.

    Reality

    Extra threads add switching and contention. Too many can slow a machine. Async and pooling exist partly to avoid that tax.

  • Myth

    Async code means no race conditions.

    Reality

    Async overlaps tasks on shared state too. Interleaving still happens between await points unless you design ownership carefully.

  • Myth

    One process can only serve one client.

    Reality

    One process can host many threads or an event loop serving thousands of connections, within memory and OS limits.

  • Myth

    Race conditions only happen on multi-core machines.

    Reality

    Interleaving on a single core still produces races. Preemption and I/O completion reorder work without needing two CPUs.

Exercises

Work these without looking up answers first. Check yourself against the intent notes.

  1. Exercise 01

    A server handles each request by blocking on a 100ms downstream HTTP call and uses a single thread with no async. Roughly how many such requests can it complete per second at saturation, and what design change overlaps the waits?

    Hint: If each request owns the only worker for the whole wait, throughput is about 1000ms / wait.

    What good looks like

    About 10 per second if strictly serial 100ms calls; async or more threads/processes overlap waiting. Mentions that CPU may be idle during waits.

  2. Exercise 02

    Two threads run: read balance, subtract 10, write balance. Start balance 100. Describe an interleaving that ends at 90 instead of 80, and name the failure.

    Hint: Write the steps as a timeline with thread A and thread B alternating.

    What good looks like

    Classic lost update race; both read 100; both write 90. Names race condition / lost update.

  3. Exercise 03

    When would you prefer multiple processes over many threads for an app server, and what do you give up?

    Hint: Think about what shared address space buys and what it risks.

    What good looks like

    Stronger isolation and crash containment; give up cheap shared memory and pay more RAM/IPC. May mention security or native-extension blast radius.

Sources & further reading

External references. Prefer primary documents and clear explainers.

Go deeper on your own

These picks go past the foundation in this lesson. Use them when you want a primary spec, official docs, or a longer walkthrough.