async / await
async / await
Section titled βasync / awaitβWhat it is
Section titled βWhat it isβasync def defines a coroutine function β calling it doesnβt run the body immediately, it returns a coroutine object that must be await-ed or scheduled to actually execute. await pauses the current coroutine at an I/O point, handing control back to an event loop, which runs other coroutines while the first one waits β all on a single thread, no GIL contention between them.
Before this feature
Section titled βBefore this featureβHandling many concurrent I/O operations (hundreds of open network connections, for example) with threading means one OS thread per connection β real but expensive overhead at scale:
import threading
def fetch(url, results, i): results[i] = blocking_http_get(url) # blocks this whole thread
results = [None] * 500threads = [threading.Thread(target=fetch, args=(u, results, i)) for i, u in enumerate(urls)]for t in threads: t.start()for t in threads: t.join()# 500 OS threads is a lot of overhead just to wait on network responsesAfter this feature
Section titled βAfter this featureβimport asyncioimport aiohttp
async def fetch(session, url): async with session.get(url) as response: return await response.text()
async def main(urls): async with aiohttp.ClientSession() as session: tasks = [fetch(session, url) for url in urls] return await asyncio.gather(*tasks) # runs all requests concurrently
results = asyncio.run(main(urls))# 500 concurrent requests on ONE thread β the event loop switches between# them at each `await` point instead of needing 500 OS threadsWhy this is better
Section titled βWhy this is betterβ- Massive I/O concurrency, low overhead: thousands of concurrent coroutines cost far less than thousands of threads β no per-thread OS stack, no GIL contention between them (thereβs only one thread).
- Explicit suspension points:
awaitmarks exactly where a coroutine can be paused, unlike threads where the OS can preempt at any instruction β this makes reasoning about shared state simpler (no lock needed between twoawaitpoints in the same coroutine, since nothing else runs during that stretch). - Purpose-built for I/O-bound, high-concurrency workloads: web servers, API clients, chat/websocket services handling many simultaneous connections.
Key notes / edge cases
Section titled βKey notes / edge casesβasync/awaitdoes not help CPU-bound work β a coroutine that neverawaits (just computes) blocks the entire event loop, starving every other coroutine; usemultiprocessing(or run CPU-bound work in a thread/process pool vialoop.run_in_executor) for that.- Calling an
async deffunction does not run it βcoro = fetch(...)just creates a coroutine object; you needawait coro,asyncio.run(coro), or scheduling viaasyncio.gather/create_taskto actually execute it. Forgetting this is the single most commonasynciomistake (RuntimeWarning: coroutine was never awaited). - Regular (blocking) I/O calls inside an
async defstill block the whole event loop β you needasync-native libraries (aiohttpinstead ofrequests,asyncpg/aiomysqlinstead of blocking DB drivers) to actually get the concurrency benefit. asyncio.gather(*tasks)runs multiple coroutines concurrently and collects all their results; awaiting them one at a time in a loop (for t in tasks: await t) would run them sequentially instead, missing the entire point.
Quick practice
Section titled βQuick practiceβ-
What does calling an
async deffunction actually return, before youawaitit?Answer
A coroutine object β the function body hasn't run yet. It only executes once awaited or scheduled on the event loop. -
Would
async/awaitspeed up a CPU-heavy loop with no I/O in it?Answer
No β with nothing to `await`, the coroutine never yields control, so it blocks the single-threaded event loop just like any blocking call would; `multiprocessing` is the right tool for CPU-bound work. -
Why does mixing a blocking (non-async) library call inside an
async deffunction defeat the purpose?Answer
A blocking call doesn't yield control at an `await` point β it freezes the entire single-threaded event loop until it returns, stalling every other coroutine that should have been running concurrently.