4 min read
Need to wake N worker threads from one controller — threading.Event, Condition.notify_all / notify(1), or a Barrier? This lab measures wake latency and wakes/s for broadcast-style sync on Linux localhost.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Pure stdlib threading. Affiliates: 0. No Docker. Timing starts after all waiters are known to be waiting (no lost-wakeup races in the Condition arms).
Verdict up front (128 waiters broadcast): Event ~8.071 ms (~15860 wakes/s); Condition.notify_all ~7.068 ms (~18110/s); Barrier ~9.247 ms. Event ping-pong handoff ~16325 ns (~61254/s).
| Arm | Pattern |
|---|---|
| Event broadcast | N waiters on Event.wait; one set() |
Condition notify_all | N waiters in cv.wait; one notify_all |
Condition notify(1) × N | wake one-by-one after all parked |
| Barrier | N workers + main Barrier.wait rendezvous |
| Event ping-pong | two threads alternate Events (handoff latency) |
Script: lab-evidence/83-threading-event-vs-condition/results/run_lab.py.
| N | Event ms | Event wakes/s | notify_all ms | notify_all /s | Barrier ms |
|---|---|---|---|---|---|
| 4 | 0.18 | 22224 | 0.097 | 41136 | 0.136 |
| 16 | 0.727 | 22016 | 0.567 | 28230 | 0.815 |
| 64 | 4.076 | 15702 | 3.548 | 18040 | 4.874 |
| 128 | 8.071 | 15860 | 7.068 | 18110 | 9.247 |
notify(1) × N (after all parked) at N=128: ~1.194 ms — useful when you must release workers one at a time, not as a faster broadcast.
Two threads alternating Event signals: ~16325 ns/handoff (~61254 handoffs/s). That is the cost of a round-trip wake for a simple flag protocol — fine for control planes, not a substitute for a lock-free queue in a hot path.
set() is hard to misuse for one-shot “go” flags.while not ready: cv.wait()).notify(1) × N is a different pattern (fair one-by-one release), not a drop-in faster notify_all.| Need | Prefer |
|---|---|
| One-shot “start now” flag | Event |
| Wait until predicate / shared state | Condition |
| All threads reach a phase gate | Barrier |
| Hand off work one worker at a time | notify(1) (or a Queue) |
Bare Condition.notify before the waiter enters wait() loses the signal. This lab parks everyone (counter + cv.wait handshake) before timing notifies. Production code should use predicate loops, not sleep-and-hope.
From N=4 → 128, Event wake wall grew ~45× while wakes/s stayed in a ~15–22k band once N≥64. That is thread create/join and scheduler fan-out, not a futex-only number. For hotter fan-out, prefer a work queue over waking idle thread armies.
clear() an Event between phases.notify(1) without holding the lock / without predicates.Evidence: summary.json, summary.txt.
One Linux box, process-local threads only. Not multiprocessing Events. Not futex microbenchmarks. Scheduler noise dominates at sub-ms wakes.
For waking N waiters, Event and Condition.notify_all are in the same band (~8.071–7.068 ms at N=128, ~15–18k wakes/s). Use Event for simple flags, Condition for predicates, Barrier for rendezvous, and treat ping-pong ~16325 ns as the two-thread handoff floor — not a throughput engine.
Lab evidence
Lab 1 Oct 2026 IST. Ran Python 3.13.5 on Linux localhost with pure stdlib threading. At N=128 broadcast waiters, Event took 8.071 ms (~15860 wakes/s), Condition.notify_all took 7.068 ms (~18110/s), and Barrier took 9.247 ms. Event ping-pong measured about 16325 ns per handoff (~61254/s). Waiters were confirmed parked before timing; no Docker, multiprocessing, or affiliate links were used.
Hands-on threading.local vs tid-keyed dict vs module-global lab: real ops/s for per-thread state access patterns, measured on Linux localhost for SREs.
Lab1 Oct 20264 min read
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Lab1 Oct 20264 min read