Async Queues (producer-consumer)
flowchart LR
A["producer"] --> B["await q.put(item)"]
B --> C{"queue full?"}
C -->|yes| D["producer SUSPENDS -- back-pressure"]
C -->|no| E["item queued"]
E --> F["await q.get()"]
F --> G{"queue empty?"}
G -->|yes| H["consumer SUSPENDS, costing nothing"]
G -->|no| I["consumer processes it"]
I --> J["q.task_done()"]
J --> K["await q.join() unblocks when every item is done"]
L["sentinel or cancellation"] --> M["is how consumers are told to stop"]
Why asyncio.Queue
Section titled “Why asyncio.Queue”asyncio.Queue enables:
- safe communication between coroutines
- backpressure (limit queue size)
Example pipeline
Section titled “Example pipeline”import asyncio
async def producer(q: asyncio.Queue):
for i in range(10):
await q.put(i)
print("produced", i)
await q.put(None) # sentinel
async def consumer(q: asyncio.Queue):
while True:
item = await q.get()
try:
if item is None:
break
print("consumed", item)
await asyncio.sleep(0.1)
finally:
q.task_done()
async def main():
q = asyncio.Queue(maxsize=5)
p = asyncio.create_task(producer(q))
c = asyncio.create_task(consumer(q))
await p
await q.join()
await c
asyncio.run(main())- Use a sentinel to stop consumers.
- Use
maxsizefor backpressure.
Check yourself
Section titled “Check yourself”-
A consumer awaits `q.get()` on an empty `asyncio.Queue`. What is it doing?
Awaiting suspends the task without blocking the thread, which is why thousands of waiting tasks are cheap while thousands of waiting threads are not.
pch.quizShowAnswer
B — Suspended — the loop runs other tasks meanwhile — Awaiting suspends the task without blocking the thread, which is why thousands of waiting tasks are cheap while thousands of waiting threads are not.
-
What does giving the queue a `maxsize` buy you?
Unbounded, a producer faster than its consumer grows the queue until memory runs out — and that failure looks like a slow leak rather than a queue problem.
pch.quizShowAnswer
B — Back-pressure — a full queue suspends the producer — Unbounded, a producer faster than its consumer grows the queue until memory runs out — and that failure looks like a slow leak rather than a queue problem.
-
What does `await q.join()` wait for?
It tracks completion of the WORK, not merely that the queue has drained. An item removed but not yet processed still counts as outstanding.
pch.quizShowAnswer
B — Every item that was put to have a matching `task_done()` — It tracks completion of the WORK, not merely that the queue has drained. An item removed but not yet processed still counts as outstanding.
-
Why use `asyncio.Queue` rather than `queue.Queue` inside an event loop?
Same reasoning as `threading.Semaphore` in async code: blocking the single thread stops the tasks that would have made progress possible.
pch.quizShowAnswer
B — `queue.Queue` blocks the thread, which stalls every task including the consumer — Same reasoning as `threading.Semaphore` in async code: blocking the single thread stops the tasks that would have made progress possible.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – asyncio.Queue Basics
Section titled “Exercise 1 – asyncio.Queue Basics”Exercise 2 – Async Producer-Consumer
Section titled “Exercise 2 – Async Producer-Consumer”Exercise 3 – Queue Size
Section titled “Exercise 3 – Queue Size”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading