Understanding Python Coroutines: From IO Multiplexing to Generators and Async/Await
This article explains how Python implements coroutines for high‑performance network and web programming by combining OS‑level IO multiplexing, generator‑based control flow, callback elimination, stack‑driven call‑chain traversal, Future objects, and the evolution toward async/await syntax.
The article begins by stating that modern Python web frameworks such as tornado now support the async and await keywords, and the author revisits the underlying principles of coroutine implementation.
It first introduces IO multiplexing as the key performance technique, showing a simplified server loop where a single thread repeatedly accepts requests, registers IO operations, and processes them without blocking:
def handler(request):
# 处理请求
pass
while True:
# 获取一个新请求
request = accept()
# 根据路由映射获取到用户写的业务逻辑函数
handler = get_handler(request)
# 运行用户的handler,处理请求
handler(request)The article then explains how traditional multithreaded servers solve blocking but incur high overhead, while IO multiplexing lets the OS notify the program when network IO is ready.
A pseudo‑code example of the OS‑level IO multiplexing API is provided:
# 操作系统的IO复用示例伪代码
# 向操作系统IO注册自己关注的IO操作的id和类型
io_register(io_id, io_type)
io_register(io_id, io_type)
# 获取完成的IO操作
events = io_get_finished()
for (io_id, io_type) in events:
if io_type == READ:
data = read_data(io_id)
elif io_type == WRITE:
write_data(io_id, data)To integrate this logic into a server, the author shows a callback‑based design where each request handler registers a callback and the event loop drives the callbacks:
call_backs = {}
def handler(req):
# do jobs here
io_register(io_id, io_type)
def call_back(result):
# 使用返回的result完成剩余工作...
pass
call_backs[io_id] = call_back
while True:
# 获取已经完成的io事件
events = io_get_finished()
for (io_id, io_type) in events:
if io_type == READ:
data = read(io_id)
call_back = call_backs[io_id]
call_back(data)
else:
# 其他类型io事件的处理
pass
# 获取一个新请求
request = accept()
handler = get_handler(request)
handler(request)Because callbacks split business logic, the article proposes using generators to turn callbacks into linear code. A handler can now yield an IO description and receive the result when the coroutine is resumed:
def handler(request):
# 业务逻辑代码...
# 需要执行一次 API 请求
result = yield io_info
# 使用 API 返回的result完成剩余工作
print(result)An example of a simple generator demonstrates the send() method and the StopIteration exception carrying the return value:
def example():
value = yield 2
print("get", value)
return value
g = example()
# 启动生成器,我们会得到 2
got = g.send(None)
print(got) # 2
try:
# 再次启动 会显示 "get 4"
got = g.send(got*2)
except StopIteration as e:
# 生成器运行完成,e.value 是返回值
print(e.value)To handle nested generators and multiple IO calls, the author builds a stack‑based wrapper that pushes each generator onto a stack, yields IO objects to the event loop, and pops generators when they finish:
def wrapper(gen):
# 第一层调用 入栈
stack = Stack()
stack.push(gen)
# 开始逐层调用
while True:
# 获取栈顶元素
item = stack.peak()
result = None
if isgenerator(item):
try:
child = item.send(result)
stack.push(child)
continue
except StopIteration as e:
result = e.value
else: # IO 操作
result = yield item
stack.pop()
if stack.empty():
print("finished")
return resultThe wrapper is used by the request handler and the event loop maintains a ready list of generators waiting for IO results. When an IO completes, its callback adds the generator back to the ready list.
ready = []
def on_request(request):
handler = get_handler(request)
g = wrapper(func1())
ready.append((g, None))
def process_ready(self):
def callback(g, result):
ready.append((g, result))
for g, result in self.ready:
io_job = g.send(result)
asyncio.get_event_loop().io_call(io_job, lambda r: ready.append((g, r)))To increase extensibility, the article introduces a Future placeholder that represents a result that will be available later. The request function now returns a Future instead of a generator, and the event loop registers a callback on the future:
class Future:
def set_result(self, result):
pass
def result(self):
pass
def done(self):
pass
def add_done_callback(self, callback):
pass
def request(url):
fut = Future()
def callback(result):
fut.set_result(result)
asyncio.get_event_loop().io_call(url, callback)
return fut
def process_ready(self):
def callback(fut):
ready.append((g, fut.result()))
for g, result in self.ready:
fut = g.send(result)
fut.add_done_callback(callback)The article then traces the historical evolution from early yield -only coroutines, through the introduction of yield from as syntactic sugar for the stack‑based wrapper, to the modern async / await syntax that hides the generator mechanics.
Finally, it compares Python’s coroutine approach with other ecosystems: JavaScript’s Promise , Go’s native goroutine scheduler, and Python libraries like gevent that patch the runtime. The conclusion emphasizes that Python’s native coroutines combine IO multiplexing with generator‑based flow control to provide high‑performance, readable asynchronous code.
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.