We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency with Threads and asyncio step 41 of 56
Build a Typed Async Wrapper for Blocking Calls
Build two helpers that let async code call blocking functions without
blocking the event loop. asyncify turns one blocking function into an async
one. gather_blocking runs a batch of blocking functions on a thread pool of
its own.
joined = asyncify(join) # join(n: int, *, sep: str = ":") -> str
await joined(7, sep="-") # "7-7", computed in a worker thread
joined.__name__ # "join"
await gather_blocking([lambda: 1, lambda: 2, lambda: 3], limit=2) # [1, 2, 3]
What to write
def asyncify[**P, T](fn: Callable[P, T]) -> Callable[P, Coroutine[Any, Any, T]]
async def gather_blocking[T](fns: Sequence[Callable[[], T]], *, limit: int) -> list[T]
asyncify(fn) returns an async function that follows these rules:
- It runs
fnin a worker thread withasyncio.to_threadand returns the result. - It passes positional and keyword arguments through unchanged.
- It keeps the
__name__and__doc__offn. Usefunctools.wraps. - An exception raised by
fnreaches the caller with its own type and message. - Its type uses the ParamSpec
P, as in the article, notCallable[..., Any].
gather_blocking(fns, limit=...) follows these rules:
- It creates its own
ThreadPoolExecutor(max_workers=limit, thread_name_prefix="gb"). - It runs each callable on that pool with
loop.run_in_executor. Await each one in its own task inside oneTaskGroup, so a failing job raises an exception group. - It returns the results in the same order as
fns. - It always shuts the pool down before it returns or raises. Call
pool.shutdown(wait=True)throughasyncio.to_thread, so the loop keeps running while the pool's threads finish. - If it is cancelled while it waits for the shutdown, it keeps waiting until
the shutdown is done, then re-raises the
CancelledError.
Edge cases
- An empty
fnsreturns[]. - A
limitbelow 1 raisesValueError, even whenfnsis empty.ThreadPoolExecutorraises it for you. - Cancelling a task does not stop a job that is already running in its thread. The driver's jobs always finish, so every shutdown completes.
What the driver reports
solve runs one scenario. Meanwhile a heartbeat task ticks five times, then
sets the signal that the blocking functions wait for.
ticks: how many times the heartbeat ticked, which is 5. If your code blocks the loop, the heartbeat cannot run. The blocking function then gives up after 3 seconds, andvaluecomes back as"stalled".value: the result of the wrappedjoincall.nameanddoc: the__name__and__doc__of your wrapper.outcomeandmessage:"ok"and"", or the type name and message of the exception the call raised.values: the listgather_blockingreturned.leftover_threads: how many threads whose names start withgbare still alive after your helper returns. It must be 0.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.