A lock protects shared state. But sometimes you need threads to coordinate โ wait for a signal, wait for a condition, cap how many run at once, or all meet at a line. That's what Event, Condition, Semaphore, and Barrier are for.
A Lock answers one question: "can I touch this shared state right
now?" But threads often need to answer richer questions โ "has startup finished yet?",
"is there an item in the buffer?", "are all 5 workers ready to begin?". Busy-waiting
in a while loop with time.sleep works but burns CPU and adds latency. The
threading module ships four primitives built for exactly this.
threading.Event โ a One-Bit Signal
An Event is a boolean flag that threads can wait on. One thread flips it
with set(); every thread blocked in wait() wakes up at once. It's the
simplest way to broadcast "go!" or "we're shutting down" to many workers.
event.set() โ flip the flag to true, wake all current waiters.event.clear() โ flip back to false; future wait() calls block again.event.wait(timeout=None) โ block until set; returns the flag's value (handy to detect timeouts).event.is_set() โ check without blocking.import threading, time
ready = threading.Event()
def worker(n):
print(f"worker {n} waiting for go signal")
ready.wait() # blocks here until ready.set()
print(f"worker {n} running!")
threads = [threading.Thread(target=worker, args=(i,)) for i in range(3)]
for t in threads:
t.start()
time.sleep(1) # let all workers reach wait()
print("main: GO")
ready.set() # one-to-many wakeup: all 3 fire at once
for t in threads:
t.join()
while not stop.is_set(): and does a chunk of work; the main thread calls
stop.set() on SIGINT. Also great for "wait until the DB connection pool is warmed up
before serving traffic."
threading.Condition โ Wait for a Predicate
An Event is a single bit. A Condition lets a thread wait for an arbitrary
predicate over shared state โ "the queue is non-empty", "the buffer has room" โ and
bundles a lock so you can inspect that state safely. It's the classic producer/consumer tool.
cond.wait() โ atomically release the lock and sleep; re-acquire the lock on wakeup.cond.notify(n=1) โ wake up to n waiters.cond.notify_all() โ wake every waiter.import threading, time, collections
cond = threading.Condition() # has its own internal lock
buffer = collections.deque()
def producer():
for i in range(5):
time.sleep(0.2)
with cond: # must hold the lock to notify
buffer.append(i)
print(f"produced {i}")
cond.notify() # wake one waiting consumer
def consumer():
while True:
with cond: # must hold the lock to wait
while not buffer: # WHILE, not if โ see below
cond.wait()
item = buffer.popleft()
print(f"consumed {item}")
if item == 4:
return
t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=producer)
t1.start(); t2.start()
t1.join(); t2.join()
while, not if: when wait() returns, the
predicate is not guaranteed to still be true. Two reasons: (1) another consumer may have been
woken first and drained the item before you re-acquired the lock, and (2) spurious
wakeups โ a waiter can legally return from wait() without any
notify(). So you must re-check the condition in a loop. The idiom is
always while not predicate: cond.wait().
wait(): you must hold the condition's lock when you call
it. wait() then atomically releases the lock and parks the thread โ so a producer
can grab the lock and notify. On wakeup, wait() re-acquires the lock before returning, so
your code inside with cond: is always running under the lock. This atomic release-and-sleep
is exactly what a hand-rolled flag + sleep loop can't do safely.
Semaphore & BoundedSemaphore โ Limit Concurrency to N
A Semaphore is a counter. acquire() decrements it (blocking at zero) and
release() increments it. Initialize it to N and only N threads can be
inside the guarded section at once โ the perfect throttle for "at most 5 concurrent connections to this
flaky API."
import threading, time
MAX_CONCURRENT = 3
gate = threading.Semaphore(MAX_CONCURRENT) # 3 permits
def call_api(n):
with gate: # acquire a permit (blocks if 0 left)
print(f"request {n} in flight")
time.sleep(1) # only 3 of these overlap at any moment
# permit released on exit
threads = [threading.Thread(target=call_api, args=(i,)) for i in range(10)]
for t in threads: t.start()
for t in threads: t.join()
BoundedSemaphore. A plain Semaphore lets you
release() more times than you acquired, silently raising the ceiling above N โ a common
bug. BoundedSemaphore raises ValueError if the count ever exceeds its initial
value, catching a stray or double release() immediately.
Lock is essentially a Semaphore(1) โ
but a lock has an owner and enforces acquire/release symmetry, while a semaphore is just a count that
any thread can release. Use a semaphore when the resource has capacity N > 1 (connection pool,
rate limiter, download slots).
threading.Barrier โ Rendezvous of N Threads
A Barrier makes exactly N threads wait for each other. Each calls
barrier.wait(); the first N-1 block, and when the N-th arrives, all are released
together. Ideal for phased computation where no thread may start phase 2 until every thread finished
phase 1.
import threading, time, random
N = 3
barrier = threading.Barrier(N)
def worker(n):
time.sleep(random.random()) # each finishes phase 1 at a different time
print(f"worker {n} done phase 1, waiting at barrier")
barrier.wait() # blocks until all N arrive
print(f"worker {n} starting phase 2") # all print together
threads = [threading.Thread(target=worker, args=(i,)) for i in range(N)]
for t in threads: t.start()
for t in threads: t.join()
Barrier is reusable โ after releasing, it resets for the next
round, so you can loop through many phases. Pass an action= callable to run once (by one
thread) at the moment the barrier trips, e.g. to merge partial results between phases. If a waiting
thread times out or is broken, the barrier enters a broken state and all wait() calls
raise BrokenBarrierError.
| Primitive | Use when | Core methods |
|---|---|---|
Event | Broadcast a one-bit signal (go / stop) to many threads | set, clear, wait, is_set |
Condition | Wait until a predicate over shared state becomes true | wait, notify, notify_all |
Semaphore | Allow at most N threads into a section | acquire, release |
Barrier | Make N threads meet at a synchronization point | wait |