A race is unsynchronized access to shared mutable state; a Lock serializes critical sections
A race condition occurs when the correctness of a program depends on the interleaving of operations from multiple threads, and some interleavings produce wrong results. The classic example is read-modify-write on a shared counter: counter += 1 compiles to a load, an add, and a store, and the GIL can switch between them. The GIL does not save you because it only guarantees single-bytecode atomicity, not statement atomicity. The fix is to guard the critical section with a threading.Lock: acquire before the read-modify-write and release after, using with lock: so the release happens even if an exception is raised. For simple counters, itertools.count with next() or a lock-free approach using multiprocessing.Value may be preferable depending on context.
Critical section: the smallest block of code that must run without interleaving.
with lock: is preferred over manual acquire/release because it guarantees release on exceptions.
Other primitives: RLock for reentrant locking, Semaphore for limited concurrency, Condition for wait/notify, Event for signaling.
Trade-off: locks serialize access, which reduces throughput. Keep critical sections small and avoid IO inside them.
Common mistake: locking the wrong object. Locks are per-instance; two threads locking different objects do not synchronize.
Common mistake: calling a function that also tries to acquire the same non-reentrant Lock and deadlocking. Use RLock when reentry is legitimate.
Version note: threading.Lock is a thin wrapper over the OS primitive and is not recursive. threading.RLock is.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience