Processes, threads and synchronization: shared memory, lost updates and lock contention
Compare process isolation with thread sharing, reproduce a deterministic lost update, and reason about mutexes, condition variables and production lock contention.
TL;DR: Threads within a process share its address space. Each thread has its own execution state and stack, but a separate stack does not make the heap private. Two request handlers can hold different local variables that refer to the same mutable object.
Linux's pthreads overview lists shared resources and per-thread attributes. Open file descriptors are shared within a process, so closing a descriptor in one thread can affect another thread using it. Separate processes normally have separate virtual address spaces, although they can deliberately share memory or communicate through sockets and pipes.
| Property | Separate processes | Threads in one process |
|---|---|---|
| Ordinary heap state | Separate address spaces | Shared address space |
| Communication | Explicit IPC or shared mappings | Shared objects plus synchronization |
| Memory corruption impact | Usually bounded by process isolation | Can damage the whole process |
| Concurrency limit | Process and resource limits | Thread limits, stacks and shared bottlenecks |
Process isolation does not isolate a shared database, filesystem or downstream quota. A hundred isolated workers can still race to perform the same external operation. Idempotency and transaction design remain necessary at those boundaries.
Reproduce the lost update
Suppose two workers increment a completed-job counter. Both read zero, calculate one, and write one. Each worker completed successfully; the counter still lost an update. The protected operation must include reading and modifying the shared value.
This Python example deliberately separates the steps and uses a barrier to force that schedule. It is a teaching reproduction, not a timing-dependent benchmark:
from threading import Barrier, Lock, Thread
def count_jobs(protected):
state = {"completed": 0}
rendezvous, mutex = Barrier(2), Lock()
def worker():
if protected:
rendezvous.wait()
with mutex:
state["completed"] = state["completed"] + 1
else:
previous = state["completed"]
rendezvous.wait()
state["completed"] = previous + 1
workers = [Thread(target=worker) for _ in range(2)]
for thread in workers:
thread.start()
for thread in workers:
thread.join()
return state["completed"]
assert count_jobs(False) == 1
assert count_jobs(True) == 2
The barrier in the protected branch runs before acquiring the mutex. Moving it inside the lock would deadlock this example: the first worker would wait for a second worker that cannot enter the locked section. Synchronization primitives can create a new failure if their ordering contradicts the schedule they require.
Python's threading documentation describes locks and barriers, along with runtime limits on parallel execution. The interpreter's execution model does not make a multi-step application invariant atomic. Runtime version, free-threaded builds and native extensions also matter when discussing CPU parallelism. Avoid transferring assumptions about one Python build to Java, Go or C.
Protect an invariant, then minimize the critical section
A mutex protects a rule only when every participating access follows the same locking discipline. For a queue, that rule might connect its item count to its stored elements. Locking the count while changing the elements without synchronization leaves the combined state inconsistent.
Keep slow network calls outside a lock where the design allows it. If a handler owns a global lock while waiting for a remote service, unrelated requests may queue behind that dependency. Simply adding threads increases the number of waiters. Where multiple locks are necessary, define a consistent acquisition order and review error paths that can exit without releasing one.
Condition variables support waiting for a state change while releasing the associated mutex. POSIX condition-wait semantics require the predicate to be checked again after waking; a notification is not a durable item in a queue. Another consumer may have consumed the available work before this thread reacquires the lock. Express the wait as a loop over the shared predicate.
Explain a production stall with evidence
For a service with low CPU and rising latency, inspect runnable versus blocked threads, lock owners and downstream waits. Compare several stack snapshots rather than assuming that one blocked frame proves a deadlock. A slow lock holder and a cyclic lock dependency need different remedies.
The application-server thread-pool guide applies this distinction to request handling. Capacity changes should follow the bottleneck: shortening a critical section may help, while doubling a pool against the same lock may only consume more memory.
Self-check: each thread has a private local reference named cart, but every reference points at the same cart object. Can two threads update the cart safely without coordination? Separate reference variables do not establish exclusive ownership of the referenced state.