本文基于 《Effective Python: 125 Specific Ways to Write Better Python, 3rd Edition》 的 第9章“并发与并行” 中的 Item 78:Maximize Responsiveness of asyncio
Event Loops with async
-friendly Worker Threads。本文旨在深入探讨如何在使用 asyncio
时,通过引入兼容异步编程模型的工作线程来避免阻塞事件循环,从而提升整体程序的响应能力。我们将从书中提供的示例出发,结合实际开发经验,分析常见的误区、解决方案以及扩展思路,帮助你写出更高效的异步代码。
无论你是正在学习 asyncio
的初学者,还是已经有一定经验但希望进一步优化性能的开发者,这篇文章都将为你提供有价值的参考。
阻塞事件循环会导致什么后果?
在编写异步程序时,一个最常见的错误就是无意中在协程中执行了阻塞操作(如 time.sleep()
、open()
、write() 等)。这些看似简单的系统调用实际上会冻结整个事件循环,导致其他任务无法及时执行。
举个例子,假设你的程序正在监听多个网络请求,并同时处理文件写入。如果你在某个协程中使用了同步的 time.sleep(1)
,那么在这1秒内,所有其他等待调度的任务都会被“卡住”。这种行为在高并发场景下尤为致命,可能导致延迟飙升、用户体验下降甚至服务不可用。
Python 提供了一个调试工具来检测这个问题:将 debug=True
参数传给 asyncio.run()
函数。当事件循环中有长时间阻塞时,它会输出类似如下信息:
Executing <Task finished name='Task-1' coro=<slow_coroutine done, defined at example.py:61> result=None created at .../asyncio/runners.py:100> took 0.506 seconds
这提示我们:该协程执行时间过长,可能是因为它阻塞了事件循环。
下面是一个典型的错误写法,直接在协程中进行文件写入:
async def run_tasks_simpler(handles, interval, output_path):
with open(output_path, "wb") as output:
async def write_async(data):
output.write(data)
async with asyncio.TaskGroup() as group:
for handle in handles:
group.create_task(tail_async(handle, interval, write_async))
虽然这段代码逻辑清晰,但它在主事件循环中执行了 open()
和 write()
操作,这些都属于同步 I/O 调用,会显著降低响应速度。
使用 run_in_executor
将阻塞操作移出主线程
为了解决上述问题,Python 提供了 loop.run_in_executor()
方法,它可以将阻塞操作提交到线程池中执行,从而释放主事件循环。
下面是改进后的版本:
async def good_coroutine():
loop = asyncio.get_event_loop()
logger.info("开始执行非阻塞协程")
await loop.run_in_executor(None, time.sleep, 1)
logger.info("非阻塞协程完成")
在这个例子中,time.sleep(1)
不再阻塞事件循环,而是交由默认的线程池执行。这样可以确保主事件循环继续调度其他任务。
同样的方式也可以用于文件操作:
async def run_tasks(handles, interval, output_path):
loop = asyncio.get_event_loop()
output = await loop.run_in_executor(None, open, output_path, "wb")
try:
async def write_async(data):
await loop.run_in_executor(None, output.write, data)
async with asyncio.TaskGroup() as group:
for handle in handles:
group.create_task(tail_async(handle, interval, write_async))
finally:
await loop.run_in_executor(None, output.close)
这种方法虽然有效,但存在两个明显缺点:
run_in_executor
。有没有更好的方法呢?我们将在下一节介绍一种更优雅的替代方案。
如何设计一个支持异步接口的线程类?
为了简化异步与同步之间的边界处理,我们可以创建一个继承自 Thread
的类,封装所有需要在后台执行的阻塞操作。这个类将拥有自己的事件循环,并对外暴露异步友好的接口。
下面是一个完整的实现示例:
class WriteThread(Thread):
def __init__(self, output_path):
super().__init__()
self.output_path = output_path
self.output = None
self.loop = asyncio.new_event_loop()
def run(self):
asyncio.set_event_loop(self.loop)
with open(self.output_path, "wb") as self.output:
self.loop.run_forever()
self.loop.run_until_complete(asyncio.sleep(0))
async def real_write(self, data):
self.output.write(data)
async def write(self, data):
coro = self.real_write(data)
future = asyncio.run_coroutine_threadsafe(coro, self.loop)
await asyncio.wrap_future(future)
async def real_stop(self):
self.loop.stop()
async def stop(self):
coro = self.real_stop()
future = asyncio.run_coroutine_threadsafe(coro, self.loop)
await asyncio.wrap_future(future)
async def __aenter__(self):
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.start)
return self
async def __aexit__(self, *_):
await self.stop()
asyncio.run_coroutine_threadsafe()
和 asyncio.wrap_future()
,实现了跨线程的异步调用。__aenter__
和 __aexit__
支持异步上下文管理,确保线程正确启动和关闭。WriteThread
并调用其 write()
方法即可,无需每次都手动切换线程池。如何将前面的技术整合成一个完整的异步应用?
有了上面的 WriteThread 类之后,我们可以轻松重构之前的 run_tasks 函数,使其完全异步化:
async def run_fully_async(handles, interval, output_path):
async with (
WriteThread(output_path) as output,
asyncio.TaskGroup() as group,
):
for handle in handles:
group.create_task(tail_async(handle, interval, output.write))
这段代码不仅结构清晰,而且完全避开了在主事件循环中执行任何阻塞操作。所有的文件读取和写入都被分配到了独立的线程中处理。
+---------------------+
| Main Event Loop |
+----------+----------+
|
| 启动 WriteThread
v
+---------------------+
| WriteThread |
| (专属事件循环) |
+----------+----------+
|
| 执行 open/write/close
v
+---------------------+
| 文件系统 I/O |
+---------------------+
本文围绕《Effective Python》第9章 Item 78 展开,详细讲解了如何通过兼容异步模型的工作线程来提升 asyncio
事件循环的响应能力。
核心要点回顾如下:
time.sleep()
、open()
、write() 等在内的同步调用会严重影响程序响应。run_in_executor
移除阻塞操作:这是最直接有效的手段,但会产生大量样板代码。这些技术在实际开发中具有重要价值,尤其是在构建高性能、高并发的服务端应用时,合理使用线程与协程的协作机制,能够显著提升系统吞吐量和响应速度。
学习 asyncio
是一个逐步深入的过程,而理解如何与线程协作,则是迈向高级异步编程的关键一步。通过本次实践,我深刻体会到:良好的架构设计不仅能提高性能,还能极大增强代码的可读性和可维护性。
如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享给你的朋友!后续我会继续分享更多关于《Effective Python》精读笔记系列,参考我的代码库 effective_python_3rd,一起交流成长!