Sync Primitives -- Pistol, Conveyor Belt, Key and Car Park
The Analogy
Tasks share one stage, so they need house rules:
- Event is a starting pistol. Runners wait on the line; one shot releases all of them at once.
- Queue is a conveyor belt. Producers put items on, consumers take them off, oldest first. A belt with a size limit makes producers wait when it is full.
- Lock is a bathroom key. One person holds it; everyone else queues in the order they arrived.
- Semaphore is a car park with a fixed number of spaces. Cars beyond the limit wait at the barrier until someone leaves.
How It Works
Every primitive keeps a line of waiting Futures in a deque. When a
task must wait it makes a future, joins the line and awaits it. When
the resource frees up, the releaser fills in the first future in line --
and hands the key (or space, or item) straight over, so nobody can
sneak in front.
acquire() -- key taken --> [ waiter1 ][ waiter2 ][ waiter3 ]
release() ---------------> waiter1 gets the key (still "locked")
The subtle part: cancellation
- A waiter cancelled while still in line is simply removed.
- A waiter cancelled after it was handed the key must pass the key
to the next in line, or it would be lost and everyone behind would wait
for ever.
Queue.get()likewise puts a handed-over item back at the front of the belt.
Try It
from pynet.loop import Queue, Semaphore, gather, run, sleep
async def main():
belt: Queue[int] = Queue(maxsize=2)
spaces = Semaphore(2)
async def producer():
for n in range(5):
await belt.put(n) # waits when the belt is full
async def consumer():
for _ in range(5):
async with spaces: # at most 2 inside at once
print("got", await belt.get())
await sleep(0.1)
await gather(producer(), consumer())
run(main())
When to Use
Event-- "start when I say go", or "tell everyone shutdown began".Queue-- messaging between tasks; the bounded form gives back-pressure so a fast producer cannot flood a slow consumer.Lock-- protect a shared thing that several awaits must update together.Semaphore-- limit concurrency. The scheduler will use it for resource pools (for example, at most N jobs running at once).
What We Test
- An event wakes every waiter;
clearre-arms it; cancelled waiters leave quietly. getwaits forput; a full bounded queue makesputwait;get_nowaitandput_nowaitraiseQueueEmpty/QueueFull.- A cancelled getter after an item was assigned does not lose the item.
- Locks are FIFO; releasing an unlocked lock raises
RuntimeError. - A semaphore never lets more than its limit in; over-releasing raises
ValueError. - A cancelled waiter never steals a slot, and one cancelled after being handed a slot or lock passes it on.
Next Up
Send slow, blocking work to helpers with Worker Threads, or revisit the Event Loop overview and Cancellation and Timeouts.