CS Essentials Deep Dive · 4 of 4

Concurrency — Many Things at Once, Without Chaos

Concurrency is structuring a program so multiple things make progress at the same time. Parallelism is actually running them on multiple cores. They sound the same and aren't. Get the model right and you ship a fast, correct service. Get it wrong and you ship Heisenbugs that vanish under a debugger.

ThreadsAsyncLocksRace ConditionsEvent Loop
← Back to Foundations
Quick Facts

The Vocabulary

Basic Concepts

  • Concurrency: dealing with many things at once (interleaved progress).
  • Parallelism: doing many things at once (simultaneous execution on multiple cores).
  • Process: isolated address space; OS-scheduled. Crashes don't take down siblings.
  • Thread: unit of scheduling inside a process; shares memory with siblings. Cheap-ish, but not free.
  • Coroutine / fiber / green thread: user-space "thread" the runtime schedules cooperatively. Cheap (microseconds, kilobytes).
  • Async / await: syntactic sugar for non-blocking I/O on top of an event loop or task scheduler.
The Models

How Languages Structure It

ModelLanguagesHow It Works
OS threads + locksC, C++, Java, C#, RustKernel-scheduled threads sharing memory. Synchronize with mutexes, condition variables, atomics.
Event loop / single-threaded asyncJavaScript (Node, browser)One thread, callback queue. Non-blocking I/O wakes callbacks. async/await hides the queue.
Coroutines / async runtimePython (asyncio), Rust (tokio), C# (Task), KotlinCooperative tasks scheduled on a small thread pool. Awaiting yields control.
Goroutines + channelsGoCheap green threads (M:N scheduler). Communicate by sending values through channels, not by sharing memory.
ActorsErlang, Elixir, AkkaIsolated processes that exchange messages. No shared memory. Failure isolation built in.
Software transactional memoryClojure, HaskellRead/write inside a transaction; runtime retries on conflict. Composable, but rare in practice.
CPU vs I/O

Which Tool, Which Job

I/O-Bound Work

Waiting on the network, the disk, a database. The CPU is idle 99% of the time. Async/event-loop wins: one thread can juggle thousands of in-flight requests because each one's "work" is mostly waiting. Threads work too, but you pay per-thread memory (1–8MB stacks) for nothing.

CPU-Bound Work

Crunching numbers, image processing, ML inference. You want actual parallelism — one thread per core. Async doesn't help; it just shuffles work on the same core. In Python, async is useless here and the GIL prevents threads from helping — use multiprocessing, native extensions, or a different language.

Mixed Workloads

Most real services. Use async for the I/O hot path; offload CPU spikes to a thread pool, worker process, or external queue. Don't run a CPU-heavy task inside the event loop — it blocks every other request waiting behind it.

Bugs

The Classic Failure Modes

Race Condition

Two threads read-modify-write the same memory and the result depends on who got there first. counter++ is not atomic — it's load, add, store. With two threads and no lock you lose updates. Fix: a mutex, an atomic, or a single-owner pattern (channels, actors).

Deadlock

Thread A holds lock 1 and waits for lock 2. Thread B holds lock 2 and waits for lock 1. Nobody moves. Prevent by: acquiring locks in a fixed global order, using try-lock with timeouts, or eliminating shared locks with message passing.

Livelock & Starvation

Livelock: threads keep changing state in response to each other but make no progress (two people stepping the same way in a hallway). Starvation: a thread never gets the resource because higher-priority ones keep cutting in. Fairness in lock implementations matters here.

Memory Visibility

Without a memory barrier, one thread's write may never become visible to another — the CPU and compiler reorder freely. Languages define a memory model (Java JMM, C++11, Go) telling you what's guaranteed. Volatile/atomic/locked operations issue the right barriers; plain reads/writes don't.

The Async Pitfalls
  • Blocking call inside an async function. One time.sleep or sync DB call freezes every other coroutine on that loop.
  • Forgotten await. Returns a Promise/Task, not a value. Languages with strict typing catch it; JS and Python won't always.
  • Unbounded concurrency. "Fire 10,000 requests in parallel" exhausts file descriptors or downstream rate limits. Bound with semaphores or worker pools.
  • Cancellation leaks. A cancelled task whose finally-block doesn't release a resource leaks it — connection pools, file handles, locks.
Tools

Synchronization Primitives

PrimitiveWhat It DoesWhen to Use
Mutex / LockOne holder at a time.Protect a critical section. Default choice.
Read-write lockMany readers OR one writer.Read-heavy workloads with infrequent writes.
SemaphoreLock with N permits.Bound concurrency (e.g., "max 50 in-flight requests").
Condition variableWait for a predicate, signal others.Producer/consumer queues, custom sync.
AtomicLock-free single-word op (CAS, increment).Counters, flags, lock-free structures.
ChannelTyped queue between goroutines/tasks.Hand off ownership; "share by communicating."
Future / Promise / TaskHandle for an async result.Composing async work; await consumes it.
Barrier / LatchWait until N parties arrive.Bulk-synchronous parallel phases, fan-in.
Practice

Rules of Thumb

Prefer Immutability

Read-only data is automatically thread-safe. Most concurrency bugs evaporate when you stop sharing mutable state. Pass copies, use persistent data structures, treat messages as values.

Hold Locks Briefly, Never Across I/O

A lock held during a network call is a queue with one server. Compute-then-lock-then-update is the pattern. If a critical section is more than ~10 lines, it's probably wrong.

Don't Roll Your Own

Every language has battle-tested concurrent collections (ConcurrentHashMap, sync.Map, Arc<Mutex<T>>, channel libraries). Custom lock-free code looks fast in microbenchmarks and ships subtle bugs that show up at 3am.

Test with Stress & Race Detectors

Go's -race, ThreadSanitizer (C/C++/Rust), Java Flight Recorder. Single-threaded tests will never find a race condition; you have to deliberately create contention. Property-based and chaos tests are worth the setup for anything concurrent.

Continue

More CS Essentials