We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← True Parallelism and the Runtime step 13 of 18
Queue Mechanics: The Feeder Thread and the Join Deadlock
This is the number one cause of “my multiprocessing script hangs at the end” — and the reason it survives code review is that it only hangs when the payload exceeds the pipe buffer, roughly 64 KiB. Every small-input test passes.
What a multiprocessing.Queue actually is
Not a queue. It is three things: an OS pipe, a collections.deque buffer,
and a background feeder thread started lazily on first put().
put() appends to the deque and returns immediately. The feeder thread then
pickles the object and writes the bytes into the pipe. A pipe has a fixed
kernel buffer; once it is full, the feeder blocks until a reader drains it.
Now read the documented deadlock:
A process that has put items in a queue will wait before terminating until all the buffered items are fed by the feeder thread to the underlying pipe.
So:
p.start()
p.join() # <- parent blocks here
result = q.get() # <- never reached
The child cannot exit until its feeder has written everything. The feeder
cannot write because the pipe is full. The pipe is full because the parent is
blocked in join() instead of reading. Three parties, no progress, no
traceback, no exit code. Always drain the queue before you join.
Everything else about Queue you will need
-
qsize(),empty()andfull()are documented as unreliable, andqsize()raisesNotImplementedErroron macOS. Never build control flow on them. -
Queuehas notask_done()/join(). That isJoinableQueue. -
SimpleQueuehas no feeder thread and no buffering —put()blocks until the write completes. That makes it the right primitive when you want backpressure rather than an unbounded in-memory buffer. -
A
Manager().Queue()is a proxy to a server process and has none of these semantics — different performance, different failure modes.
Your task
Build the shutdown protocol that is correct by construction.
def solve(*, items: list[int], workers: int) -> tuple[tuple[int, ...], int]:
Start workers consumers on a shared inbox. Each consumes ints until it sees
the None sentinel, then posts exactly one Done(worker=index) to the outbox
and returns. The parent puts all the items, then one sentinel per worker,
then drains the outbox — collecting results and counting Done messages —
and only joins the threads once it has seen workers of them.
Return the sorted results (each item squared) and the sentinel count.
qsize() must never appear in your solution. Counting sentinels is what
replaces it, and it is the technique that works on every platform.
Two cases that catch a half-built protocol: items=[] with workers=4 must
still return four sentinels, and workers greater than len(items) must not
leave a worker blocked on an empty inbox forever.
The typing lesson
The sentinel’s type is a design decision.
Queue[Item | None] # every consumer, forever, must handle None
Queue[Item | Done] # isinstance(msg, Done) narrows to exactly two branches
A Done dataclass carries information — which worker finished — costs
nothing, and narrows cleanly. None is the sentinel you reach for when you
have not decided what the message means yet, and it leaks that indecision into
every consumer downstream.
The other habit worth forming: annotate the queue explicitly.
multiprocessing.queues.Queue[Item], never the bare class, and never letting
inference hand you Queue[Any] from a ctx.Queue() call.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.