Guide

Concurrency means several things making progress at once. It shows up as threads in one process, separate processes, or many machines. The trouble starts when two of them read and write the same data. The result then depends on the exact order of their steps, and that order changes from run to run.

Most concurrency bugs are not exotic. A counter drops increments, a check passes and then the action fails, or two services each wait for the other forever. Learning a small set of patterns handles most of them.

The big idea

Two ideas explain nearly every concurrency bug:

  1. Interleaving. Each thread runs a sequence of steps, and the operating system can pause one thread between any two steps. Read, modify and write look like one operation in your code. They are three steps to the machine.
  2. Visibility. On modern CPUs, one core's writes may not be visible to another in the order you expect. Caches and reordering mean that without synchronization, you can't assume what another thread sees.

Locks solve both by letting only one thread into a section at a time, and by publishing its writes when it leaves.

Concepts in this topic

StepConceptWhat it answers
1Race conditionsWhy do two correct-looking threads lose an update?
2Mutexes, semaphores and condition variablesWhich primitive fits which waiting pattern?
3Atomics and memory orderingWhen can one word be updated without a lock?
4Deadlock and livelockHow do threads wait forever, and how do you prevent it?
5Shared state without locksHow do message passing, database constraints and fencing tokens help?

The same bugs appear across machines. Two servers can both believe they hold a lock, which is why fencing tokens exist, covered in the last concept page. Async I/O has its own concurrency model, covered in async I/O.

Common mistakes

  • Using a lock around one line of a multi-step update, and missing the step that reads the state.
  • Holding a lock during a network call, so every other thread waits on a remote service.
  • Assuming a test that passes once proves the code is correct. Race bugs often need a specific timing.
  • Reaching for lock-free code before measuring whether a lock is the bottleneck.

Key takeaways

  • A race is any result that depends on the order of steps, and it's often a read-modify-write.
  • Pick the primitive by the pattern: a mutex for exclusive access, a condition variable for waiting, atomics for one word.
  • Keep critical sections short, and never hold a lock across a slow call.
  • Across machines, use database constraints or fencing tokens, because a local lock doesn't protect remote state.

Further learning

Next, read race conditions to see how a simple read-modify-write goes wrong.