Concurrency

Concurrent code needs rules for sharing state and making changes visible. Those rules depend on the language's memory model.

Data races

In C and C++, conflicting access to a non-atomic object without the required synchronisation is undefined behaviour. Other languages have different guarantees, but unsynchronised access can still produce incorrect results.

Passing a stress test does not show that a program is race-free.

Happens-before is the primitive

Happens-before describes ordering between operations. Mutexes, thread joins and suitable atomic operations can establish it.

In C++, an acquire load that reads from a release store can observe the writes preceding that store. Sequentially consistent atomics also participate in a single total order. Relaxed atomics provide atomic access without ordering unrelated data.

Use the simplest ordering that you can justify. Hardware costs vary, so avoid choosing a weaker ordering from assumptions about speed.

Deadlock and the lock ordering discipline

Acquire multiple locks in a consistent order. Avoid calling unknown code while holding a lock: it may acquire another lock or call back into yours.

Keep lock ownership and the protected data clear. Short critical sections reduce contention.

Lock-free, and what it costs

Lock-free algorithms provide system-wide progress; wait-free algorithms provide a progress guarantee for each operation. Neither guarantees better performance than a mutex.

Pointer-based structures also need safe memory reclamation. Removing a node does not mean another thread has stopped using it. ABA occurs when a value changes and returns to its earlier value, hiding the intervening change from a comparison.

Where the time actually goes

Threads can contend on a shared counter or even on separate values in the same cache line. Per-thread state can reduce this traffic.

ThreadSanitizer can find races in the code paths a run exercises. It does not prove the absence of races.

Alternatives to shared mutation

Immutable data and message passing reduce shared mutable state. Publishing data or transferring ownership still needs the guarantees provided by the language or channel.

See OCaml Internals for OCaml 5's domains and runtime.

Concurrency and parallelism

Concurrency lets operations overlap in progress. Parallelism executes work at the same time. An event loop can handle many waiting operations on one thread; it does not make CPU-heavy work run in parallel by itself.

Choose the unit of ownership before adding workers. Data owned by one worker is easier to reason about than data every worker can mutate.

Queues and backpressure

A queue separates producers from consumers. If producers remain faster, an unbounded queue can turn overload into memory growth. A bounded queue needs a policy: wait, reject work, drop selected items or reduce production.

Measure queue length and waiting time. A worker pool can be busy while throughput stays flat because workers are waiting on the same bottleneck.

Cancellation and shutdown

Define who requests cancellation, where workers check it and what cleanup happens. Stop accepting new work before draining or cancelling outstanding work. Joining workers gives the owner a clear point at which they have finished.

Timeouts limit waiting; they do not prove the underlying operation stopped. Retrying an operation after a timeout can duplicate its effect unless the interface handles that possibility.

Checking an invariant

Write down which lock, channel or ownership rule protects each shared value. Check the rule across error paths and callbacks as well as the successful path.

Reading