Reference · Concurrency

Multithreading & concurrency explained

Threads vs processes, the race conditions that bite you, how locks and deadlocks work, and how to write code that’s actually thread-safe.

Threads vs processes

A process is a running program with its own isolated memory. A thread is a unit of execution inside a process; threads in the same process share memory. That shared memory is what makes threads cheap to communicate — and also what makes concurrency hard. Multiple threads reading and writing the same data at the same time is the root of most concurrency bugs.

Concurrency vs parallelism

They’re different. Concurrency is structuring a program as independent tasks that can make progress in overlapping time periods. Parallelism is actually running tasks at the same instant on multiple CPU cores. You can have concurrency on a single core (the scheduler interleaves tasks); parallelism needs multiple cores. Concurrency is about design; parallelism is about execution.

Race conditions

A race condition happens when the result depends on the unpredictable timing of threads. The classic example is two threads incrementing a shared counter: read, add 1, write back is three steps, and if both threads read the same value before either writes, one increment is lost. The bug is intermittent and timing-dependent, which is why concurrency bugs are so hard to reproduce.

Locks, mutexes, and critical sections

The fix is to make the dangerous sequence atomic — indivisible. A lock (or mutex, “mutual exclusion”) ensures only one thread enters a critical section at a time; others wait. Hold locks for as short a time as possible: locking too much serializes your program and kills the benefit of threads, while locking too little leaves races open.

Deadlocks

A deadlock is when threads wait on each other forever: thread A holds lock 1 and wants lock 2, while thread B holds lock 2 and wants lock 1. Neither can proceed. The standard prevention is to always acquire multiple locks in the same global order, keep the number of simultaneously held locks small, and use timeouts so a stuck acquisition fails loudly instead of hanging.

Writing thread-safe code

  • Don’t share mutable state if you can avoid it — the safest shared data is no shared data.
  • Prefer immutability: data that never changes after creation is automatically thread-safe.
  • Use higher-level tools: thread-safe collections, atomic types, and message-passing (queues / channels) are easier to get right than hand-rolled locks.
  • Protect every access to shared mutable data with the same lock — guarding only some accesses still races.

Async vs threads

Not all concurrency needs threads. For I/O-bound work — waiting on the network, disk, or a database — async / non-blocking models (event loops, async/await) let a single thread juggle thousands of in-flight operations without the overhead and locking of real threads. Reach for threads (or multiple processes) for CPU-bound work that needs real parallelism across cores; reach for async for I/O-bound work that’s mostly waiting.

FAQ

Why is my concurrency bug so hard to reproduce?

Race conditions depend on exact timing between threads, which varies run to run. Stress tests, thread sanitizers, and adding logging (which itself changes timing) are how you flush them out.

Is more threads always faster?

No. Past the number of CPU cores, extra threads add scheduling and locking overhead. For I/O-bound work, async usually scales better than piling on threads.

What’s a critical section?

The span of code that accesses shared data and must not run in two threads at once. You protect it with a lock so only one thread is inside at a time.

Related

See OOP concepts and memory leaks, or browse the full Reference.