Blocking code and to_thread
The event loop runs on one thread. A coroutine that calls a blocking function, such as time.sleep, a big file read or a library without async support, freezes every other task until that function returns.
asyncio.to_thread(func, *args) runs the function in a worker thread and gives you something to await. Meanwhile the loop keeps running other tasks.
import asyncio
import time
def read_sensor(n):
time.sleep(0.01) # blocking
return n * 10
async def main():
values = await asyncio.gather(
asyncio.to_thread(read_sensor, 1),
asyncio.to_thread(read_sensor, 2),
)
print(values) # [10, 20]
asyncio.run(main())Waiting versus working
This helps with blocking calls that wait. Pure Python computation is different: threads take turns holding the GIL, so to_thread keeps the loop responsive but does not finish the work any sooner. Spreading computation over several cores takes separate processes, which a desktop Python offers through ProcessPoolExecutor.
Your task
Write async def read_all(paths, read), where read(path) is an ordinary blocking function. Run every read at the same time in worker threads, keep the event loop free while they run, and return the results as a list in the order of paths.
The explanation opens here once every check passes.