Concurrency, the GIL and asyncio
Pick the right tool for the workload: processes for CPU-bound Python, threads for blocking I/O, asyncio for many concurrent waits. Then know exactly how errors, cancellation and shared state behave in each.
Key points
- 1
The GIL lets one thread run Python bytecode at a time; blocking I/O and many C extensions (hashlib, zlib, NumPy) release it, so threads still help for I/O and native work.
- 2
The GIL does not make your code thread-safe:
x += 1, check-then-act and other read-modify-write sequences need athreading.Lock. - 3
multiprocessing pickles functions and arguments: use top-level functions, guard the entry point with
if __name__ == "__main__":(required with spawn, the default on Windows and macOS), and batch tiny tasks withchunksize. - 4
In asyncio a coroutine only yields at an
awaitthat really suspends;time.sleepor a synchronous driver blocks the whole loop, so offload withasyncio.to_thread. - 5
gatherkeeps argument order and does not cancel siblings on error;TaskGroup(3.11+) cancels siblings and raises anExceptionGrouphandled withexcept*. - 6
Cancellation is a
CancelledError(aBaseExceptionsince 3.8) raised at theawait; always re-raise it, or timeouts and TaskGroups stop working. - 7
Keep references to tasks from
create_task(or use a TaskGroup), and always consume futures withresult(), or exceptions are silently lost.
Common traps
Calling a coroutine function without awaiting it does nothing except warn "coroutine was never awaited".
cache.setdefault(key, load(key))still callsloadevery time, because arguments are evaluated first.A thread-pool task that waits on another task submitted to the same full pool deadlocks.
Read the source
Test yourself on Concurrency, the GIL and asyncio
Ten questions, with the answer and explanation after each one.