Events, conditions and semaphores

threading & multiprocessing · Practical · 11 min

Signalling, rate limiting and rendezvous points are common needs that locks alone handle badly.

Beyond Lock, threading offers higher-level primitives:

  • `Event`: a boolean flag. event.set() wakes every thread blocked in event.wait(timeout); clear() resets. Perfect for "stop now" and "ready" signals. A worker loop can check while not stop.is_set(): and sleep with stop.wait(interval), so it wakes immediately on shutdown instead of after a full sleep.
  • `Semaphore(n)`: a counter allowing up to n holders. Use with sem: to limit concurrent access to a resource, such as at most 5 simultaneous connections. BoundedSemaphore raises if released more than acquired.
  • `Condition`: a lock plus wait/notify. with cond: cond.wait_for(predicate) sleeps until another thread changes state and calls cond.notify() / notify_all(). It's the building block of queues; prefer queue.Queue unless you need custom conditions.
  • `Barrier(n)`: n threads call wait() and all are released together, for phased computations.
  • `RLock`: a lock the same thread may acquire repeatedly.

All waits take timeout=. Use timeouts in production code so a bug shows up as a timeout error rather than a silent hang. Never poll in a tight loop (while not flag: pass): it burns CPU and, with the GIL, slows the very thread you're waiting for.

Example

import threading, time

stop = threading.Event()
slots = threading.Semaphore(2)
active, peak, lock = 0, 0, threading.Lock()

def poller():
    ticks = 0
    while not stop.wait(0.05):          # sleeps, but wakes at once on stop.set()
        ticks += 1
    print("poller stopped after", ticks, "ticks")

def use_resource():
    global active, peak
    with slots:                         # at most 2 threads inside
        with lock: active += 1; peak = max(peak, active)
        time.sleep(0.1)
        with lock: active -= 1

p = threading.Thread(target=poller); p.start()
users = [threading.Thread(target=use_resource) for _ in range(6)]
for u in users: u.start()
for u in users: u.join()
stop.set(); p.join()                    # poller stopped after ~6 ticks
print("peak concurrent users:", peak)   # peak concurrent users: 2

Remember

  • Event signals a flag to many waiting threads; wait(timeout) doubles as interruptible sleep
  • Semaphore(n) caps concurrent access
  • Condition and Barrier handle custom state changes and rendezvous

Watch out for

  • Busy-waiting on a shared boolean instead of using Event.wait().

Check yourself

  1. Why use stop.wait(5) instead of time.sleep(5) in a worker loop?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/threading.html#event-objects