To share data safely across threads you coordinate access with primitives — mutexes, semaphores, condition variables. Learn what each does, and the deadlocks that misusing them causes.
A mutex (mutual exclusion lock) is the fundamental synchronization tool: only one thread can hold it at a time, so code between acquiring and releasing it — the critical section — runs without interference. Wrapping shared-state access in a mutex fixes the race from the previous lesson. The cost is contention: threads waiting for the lock are idle, so you keep critical sections small.
import threading
counter = 0
lock = threading.Lock() # a mutex
def increment():
global counter
for _ in range(100_000):
with lock: # only ONE thread in here at a time
counter += 1 # the critical section — now safe
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 400000, every time — the race is gone
# keep the critical section SMALL: threads waiting on the lock do no work
# (contention). lock only the shared access, not unrelated computation.Beyond the mutex, a family of primitives coordinates threads in different ways. A semaphore permits up to N threads at once (limiting concurrency, like a connection pool). A condition variable lets threads wait until some state becomes true and be woken when it does. A read-write lock allows many readers or one writer. A spinlock busy-waits instead of sleeping, for locks held very briefly. Each fits a specific coordination need.
The synchronization toolkit — pick by what you need to coordinate:
MUTEX exactly ONE thread in the critical section. (the default)
SEMAPHORE up to N threads at once — limit concurrency
(e.g. "max 100 DB connections", a resource pool).
CONDITION VAR wait until a condition is true; another thread signals it
("wait for the queue to be non-empty" — see next topic).
READ-WRITE LOCK many concurrent READERS or one exclusive WRITER
(good for read-heavy shared data).
BARRIER N threads all wait until every one reaches a point.
SPINLOCK busy-WAIT (don't sleep) for VERY brief locks — no context
switch, but burns CPU. Use only for tiny critical sections.A condition variable solves "wait until something is ready" without busy-waiting. The canonical use is producer-consumer: worker threads wait on a condition until the work queue is non-empty, and a producer signals the condition when it adds an item. In practice a thread-safe queue wraps this pattern for you, and it is how a server hands network events to a pool of worker threads efficiently.
import threading, collections
queue = collections.deque()
cond = threading.Condition()
def producer(item):
with cond:
queue.append(item)
cond.notify() # wake one waiting consumer
def consumer():
while True:
with cond:
while not queue:
cond.wait() # sleep until notified (no busy-waiting)
item = queue.popleft()
process(item) # work outside the lock
# producer-consumer: workers sleep until there's work, then wake instantly.
# (queue.Queue wraps exactly this — use it rather than hand-rolling.)Locks introduce their own hazard: deadlock, where two threads each hold a lock the other needs and both wait forever. The classic cause is acquiring multiple locks in different orders. The standard prevention is a global lock ordering — always acquire locks in the same sequence — plus keeping locking simple and using timeouts. Deadlocks freeze a server hard, so avoiding them is a core skill of lock-based concurrency.
import threading
lock_a, lock_b = threading.Lock(), threading.Lock()
# DEADLOCK: thread 1 takes A then B; thread 2 takes B then A -> both wait forever
def thread1():
with lock_a:
with lock_b: ... # waits for B (held by thread2)
def thread2():
with lock_b:
with lock_a: ... # waits for A (held by thread1)
# FIX: always acquire locks in the SAME global order (e.g. always A before B)
def safe():
with lock_a:
with lock_b: ... # everyone follows this order -> no cycle
# rules: consistent lock ORDER, hold few locks, keep critical sections short,
# use timeouts. deadlock freezes the server solid -> design to prevent it.