We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency Without Regret step 7 of 38
run_workers: results, errors and a join deadline
Give threads the two things threading.Thread does not give you: a result and
an error.
def run_workers[T](fns: Sequence[Callable[[], T]], timeout: float) -> list[T]:
- Run each callable in its own non-daemon thread.
-
If the join window of
timeoutseconds expires with any thread still alive, raiseTimeoutError. - Otherwise, if any worker raised, re-raise the exception from the lowest-indexed failing worker, with its traceback intact.
-
Otherwise return one result per callable, in
fnsorder.
The precedence matters and is deliberate: a hang outranks a failure, because you cannot honestly report results you have not finished collecting. “Lowest-indexed” rather than “first to fail” is what makes the outcome reproducible — whichever thread the OS happened to schedule first, the same input produces the same exception.
Why this exists
Thread(target=..., args=...) has no return channel. When run() raises, the
exception goes to threading.excepthook, which prints to stderr and returns;
the thread dies and your program carries on believing the work happened.
join() returns normally either way — join means “finished”, not “succeeded”.
That is the bug where a background refresher stops working and nobody notices
for three weeks.
What the report proves
-
values— results in input order. The successful workers sleep longest-first, so completion order is the reverse of input order. Appending results as they finish fails here. -
outcome/message— the right exception type and the worker’s own message, not a re-wrapped string. -
traceback_ok— the propagated traceback still contains the frame_explodethat actually raised.raise ValueError(str(original))loses that and fails this check; re-raising the caught exception object keeps it. -
any_daemon— must beFalse. A daemon thread is stopped abruptly at interpreter exit: nofinally, no__exit__, no flush. -
on_main/threads— every callable really ran on its own thread, never the caller’s.
Where the type system earns its keep
run_workers is generic in T, so run_workers(fns_returning_int, 1.0) is a
list[int]. Compare that to Thread(target=fn, args=(...)), where args is
effectively untyped — nothing checks that the tuple matches the target’s
signature, and nothing describes what the target returns, because there is
nowhere for a return value to go.
Collecting results is where --strict pushes back: results: list[T] = [None] * n does not type-check, and it should not, because None is not a T. A
dict[int, T] filled by index and read back with a comprehension is the clean
answer, and it is also what makes “one result per callable, in order” a
statically visible property rather than a comment.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.