Mixing Async and Blocking - Middle¶
Put each kind of work on an execution resource suited to it.
async def poll_job(job_id):
# Suitable for a legacy blocking network SDK.
return await asyncio.to_thread(warehouse_sdk.get_status, job_id)
to_thread suspends the coroutine while a pool thread blocks. It protects the event loop, but does not make the call cheaper or automatically limit how many calls are queued.
| Work | Mechanism | Why |
|---|---|---|
| Async Kafka/object-store client | await directly | Runtime owns readiness |
| Legacy blocking network client | bounded thread pool | Waiting releases loop |
| Python CPU transform | process pool/Spark | Threads remain GIL-limited |
| Native code releasing GIL | measured thread pool | Can use cores, but verify |
limit = asyncio.Semaphore(16)
async def bounded_poll(job_id):
async with limit:
return await asyncio.to_thread(warehouse_sdk.get_status, job_id)
The semaphore bounds admitted work rather than allowing an executor's hidden queue to absorb overload. Preserve deadlines: a timeout around the await may stop waiting while the underlying thread and remote call continue.
Test yourself¶
- Why is a thread pool suitable for blocking I/O but often not Python CPU work?
- What does the semaphore protect that
to_threadalone does not? - Does cancelling the await forcibly terminate the pool thread?
Continue to senior.md.