Modern Python Coroutine Programming Guide
In the world of Python concurrent programming, coroutines have always been a powerful but slightly subtle existence. From the "black technology" implemented by early generators to today's elegantasync/awaitSyntactic sugar, the evolution of coroutines not only reflects the growth of the language, but also marks the overall popularity of the asynchronous programming paradigm. This article will take you from basic concepts to advanced patterns to fully master modern Python coroutines.
Basic concepts of coroutines
Coroutine (Coroutine), sometimes also called micro-threads or fibers, is a concurrent execution unit that is more lightweight than threads. The biggest difference between it and ordinary functions (subprograms) is that it can actively suspend itself during execution and be resumed later by external code.
Coroutine vs subroutine
Advantages of coroutines
- Lightweight switching: Coroutine switching is completely controlled by the program, and there is no system overhead of thread switching.
- No lock mechanism required: Because it is only scheduled within the same thread, the natural "single thread" avoids race conditions and does not require complex locks.
- High concurrency: One thread can drive thousands of coroutines "running" at the same time.
- Write asynchronous logic like synchronous code:
awaitThe process is paused without blocking the entire thread, greatly improving code readability.
Coroutine evolution in Python
The Python community has been exploring coroutines for a long time and has gone through three main stages along the way.
Phase 1: Generator coroutine (Python 2.5+)
existasync/awaitBefore its birth, people used generators toyieldFeatures cleverly simulate coroutine behavior:
Although the coroutine at this stage can be used, it needs to be manuallysend、close, and passed back and forth between multiple generators, the code is obscure and error-prone.
Second stage: asyncio +yield from(Python 3.4+)
Python 3.4 introduces the standard libraryasyncio, officially incorporating the coroutine into the "official system". At that time we used@asyncio.coroutinedecorators andyield fromgrammar:
yield fromIt simplifies the calling of sub-coroutines, but it is still not intuitive enough.
The third stage: modernasync/awaitCoroutines (Python 3.7+)
Python 3.5 introducedasync/await, 3.7 once again simplifies the way to start the event loop, presenting the asynchronous code style we are familiar with today:
At this point, coroutine programming finally becomes natural, and the sequential sense of synchronized code is perfectly preserved.
Core concepts of coroutines
To truly make good use of modern coroutines, you need to understand several key roles behind them.
1. Event loop
The event loop is like a central scheduler. When a coroutine executesawaitWhen, it will tell the event loop: "I need to wait for a while, you can handle other coroutines first." The event loop will switch control to other ready coroutines, and then resume it when the suspension condition is met.
asyncio.run()It will create an event loop, run the incoming coroutine, and automatically clean up after completion. It is the entry point of most programs.
2. Waitable objects
existawaitOnly awaitable objects (Awaitable) can be placed after the keyword. There are three main categories in Python:
- coroutine object: by
async defThe object returned by the function call must beawaitOr it will be executed only when packaged into tasks. - Task (Task): Passed
asyncio.create_task()An object that wraps a coroutine. Once the task is created, it will be immediately scheduled for execution by the event loop. You can thenawaitIt gets results. - Future: represents an unfinished result. It is mostly used in underlying libraries and is not often directly contacted in ordinary application development.
3. Coroutines and tasks
The coroutine object itself will not actively run. You need to decide whether to wait for it directly or turn it into a task.
if onlyawaitA coroutine is equivalent to waiting until it ends before continuing; creating a task is equivalent to "launching and forgetting", and then getting the results when the results are needed, which provides the basis for concurrency.
Advanced coroutine mode
1. Concurrent execution of coroutines
When there are a batch of independent asynchronous tasks, you can useasyncio.gatherLet them run simultaneously:
If you want to more flexibly control the timing of task completion, you can useasyncio.wait, which returns the first completed task:
2. Combination of coroutine and thread pool
Coroutines are suitable for IO-intensive tasks, but if you have to use some synchronous blocking libraries in your code (such astime.sleep, file reading and writing, etc.), calling directly will slow down the entire event loop. At this time, the blocking operation can be handed over to the thread pool:
For CPU-intensive tasks, you can also use a similar method to hand them over to the process pool, but the more common approach is to useconcurrent.futures.ProcessPoolExecutor。
3. Coroutine timeout control
In actual projects, network requests or external services may not respond for a long time. We can set a timeout for the coroutine to prevent the task from getting stuck forever.
Tip: After Python 3.11, you can also use the more elegant
async with asyncio.timeout(1.0):Context manager, the effect is similar.
Best Practices
Mastering the following principles in daily development can help you write stable and efficient coroutine code:
-
Never call synchronous blocking functions in coroutines Use it if you want to pause
asyncio.sleep(), when encountering synchronous IO, throw it to the thread pool. -
Choose concurrency tools reasonably
asyncio.gatherSuitable for "want it all",asyncio.waitGreat for "just one" or fine control over the order of completion. -
use
async withManage asynchronous resources Resources such as database connections and files should use asynchronous context managers to ensure clean shutdown. -
Don’t forget to catch exceptions If a coroutine throws an exception but nowhere
awaitIt, exceptions will be "swallowed" by the event loop, leading to hard-to-find bugs. Be sure to be in the right placetry/except。 -
Enable debug mode to detect missed During development, you can pass
asyncio.run(coro, debug=True)Or set environment variablesPYTHONASYNCIODEBUG=1, causing asyncio to warn about coroutines that are not awaited after they are created.
Summarize
Modern Python coroutines passasync/awaitProvides us with a clear and intuitive asynchronous programming model. Compared with the ancient generator coroutine, it brings not only simple syntax, but also complete concurrency control capabilities and deep integration with the asynchronous IO ecosystem.
As computer scientist Donald Knuth said: "Subroutines are just a special case of coroutines." By mastering coroutine thinking, your code can more easily cope with high-concurrency IO scenarios - from web crawlers, API services to real-time communications, it can run fast and stably.
I hope this guide can help you open the door to coroutine programming and make your Python asynchronous code even more powerful.

