
文章目录
开篇导读
学习目标
完成本节课后,你将能够:
- 透彻理解事件循环在asyncio中的核心地位与运行机制
- 掌握事件循环的轮询底层原理(select/poll/epoll/IOCP)
- 明白不同操作系统下事件循环策略的差异与选择
- 能够手动创建、启动、关闭事件循环,而不是只会
asyncio.run() - 深入阅读asyncio事件循环的部分源码,理解关键调度流程
- 解决开发中85%以上由事件循环引发的错误
适用人群
- 已经能用asyncio写简单并发代码,但感觉内部像个黑盒
- 面试中被问到“事件循环工作原理”时只能支支吾吾
- 想彻底弄懂
run_until_complete、run_forever、stop()等API的底层行为 - 希望在生产环境中排查事件循环相关故障(如“Event loop is closed”)
学完收益
- 真正掌握异步编程的“发动机”——事件循环,不再只停留在API层面
- 理解为什么某些写法会出错(如跨线程调用事件循环)
- 具备自定义事件循环、集成第三方IO的能力基础
- 遇到asyncio诡异报错时,知道从事件循环角度入手排查
- 为后续阅读asyncio更底层源码(第17课)做好铺垫
前置知识:面向对象与Python基础
在深入之前,确保你已熟悉以下概念:
- 类与继承:事件循环是一个抽象基类(
AbstractEventLoop),具体实现在不同平台有不同的子类。 - 生成器与迭代器:早期asyncio基于
yield from,现代虽然用async/await,但底层仍依赖生成器的send和throw。 - 文件描述符(fd):Linux/Unix下一切皆文件,socket、管道、标准输入输出都有对应的整数fd。事件循环本质是监听这些fd上的事件。
- 回调函数:事件循环大量使用回调,理解回调的执行时机很重要。
推荐提前阅读
selectors模块:标准库中IO多路复用的高级封装。concurrent.futures中的Future:asyncio的Future类似,但专为事件循环设计。
一、事件循环是什么?核心作用
1.1 定义
事件循环(Event Loop) 是asyncio的核心调度器。它负责:
- 注册 协程、任务、回调。
- 监控 IO事件(socket可读/可写)、定时器(sleep到期)。
- 分发 事件:当某个事件就绪时,调用对应的回调或恢复协程执行。
- 管理 任务的创建、取消、异常传播。
通俗比喻:事件循环就像一个超级耐心的餐厅经理,他手里有一堆订单(协程任务)。他不断扫视厨房(操作系统内核)各个出菜口(IO事件)和闹钟(定时器)。哪个订单的菜好了、哪个订单的等待时间到了,他就立刻把菜送到对应的服务员手里(唤醒协程)。没有订单就绪时,他就站着休息(阻塞等待),但不会离开岗位。
1.2 为什么事件循环是关键?
在同步编程中,程序控制流完全由代码顺序和函数调用栈决定。而在异步编程中,控制权频繁在不同任务之间转移,需要某种机制来“决定下一秒该运行谁的代码”。事件循环就是那个仲裁者。
没有事件循环:协程本身只是可暂停的函数,不知道何时恢复执行。
有了事件循环:协程通过await让出控制权,事件循环负责在条件满足时重新唤醒它。
1.3 事件循环的最小伪代码
class SimplestEventLoop:
def __init__(self):
self.tasks = [] # 待执行的任务队列
self.ready = [] # 就绪的任务(比如sleep结束)
def run_forever(self):
while True:
# 1. 收集所有就绪的事件
self.wait_for_events() # 阻塞,直到至少一个事件就绪
# 2. 执行所有就绪任务对应的回调
for callback in self.ready:
callback()
真实的asyncio事件循环远比这个复杂,但核心骨架如此。
二、事件循环运行机制深度拆解
2.1 状态机视角
事件循环本质上是一个状态机,有以下几个关键状态:
- NEW:事件循环刚被创建,尚未启动。
- RUNNING:正在运行(
run_forever或run_until_complete被调用)。 - STOPPING:
stop()被调用,正在优雅关闭。 - CLOSED:
close()被调用,资源释放。
import asyncio
loop = asyncio.new_event_loop() # NEW 状态
print(loop.is_running()) # False
loop.run_until_complete(asyncio.sleep(0)) # RUNNING
loop.close() # CLOSED
2.2 心跳循环详解
事件循环每次迭代(称为一次“tick”)大致执行以下步骤(以uvloop或标准库BaseEventLoop为参考):
开始tick:
1. 调用 _run_once(timeout) ← 核心
1.1 计算下一次定时器的到期时间,作为 IO 多路复用的最大等待时间
1.2 调用 selector.select(timeout) 等待 IO 事件就绪
1.3 将所有就绪的 IO 回调加入就绪队列
1.4 处理所有到期的定时器回调(如 asyncio.sleep 完成)
1.5 执行就绪队列中的所有回调(每个回调可能生成新的任务)
2. 处理信号(仅 Unix)
3. 检查是否需要停止(如 stop() 被调用或 run_until_complete 的 future 完成)
4. 进入下一次循环
源码位置(CPython 3.11):Lib/asyncio/base_events.py 中的 _run_once 方法。
2.3 重要:事件循环是“协作式”而非“抢占式”
- 协程只在遇到
await时可能切换;如果协程内部没有await(例如死循环),它会永远霸占事件循环,导致其他任务饿死。 - 事件循环不会主动暂停一个长时间运行的协程。这是与操作系统线程调度(抢占式)最大的不同。
async def bad():
while True:
pass # ❌ 没有 await,事件循环永远无法切换出来
async def good():
while True:
await asyncio.sleep(0) # ✅ 主动让出控制权
三、轮询机制原理:从select到epoll/IOCP
事件循环需要一种方式高效地等待多个IO事件。这就要用到操作系统提供的IO多路复用接口。
3.1 轮询的历史演进
| 接口 | 工作方式 | 时间复杂度 | 最大fd数 | 适用平台 |
|---|---|---|---|---|
select | 线性扫描fd集合 | O(n) | 1024(通常) | 类Unix、Windows |
poll | 链表管理,仍线性扫描 | O(n) | 无硬限制 | 类Unix |
epoll | 事件驱动,只返回就绪fd | O(1) | 系统最大fd数 | Linux |
kqueue | 类似epoll的事件驱动 | O(1) | 系统限制 | BSD/macOS |
IOCP | 完成端口,异步IO模型 | O(1) | 系统限制 | Windows |
asyncio的选型策略:
- Windows:默认
ProactorEventLoop(基于IOCP,支持真正的异步套接字IO)。 - Linux:默认
SelectorEventLoop,自动选择最高效的轮询器(epoll → poll → select)。 - macOS/BSD:SelectorEventLoop 使用 kqueue。
3.2 epoll 核心流程(Linux)
// 伪代码,非Python
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ev);
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, timeout);
for (i=0; i<n; i++) {
if (events[i].events & EPOLLIN) {
// 这个 socket 可读,唤醒对应的协程
}
}
}
asyncio 使用 selectors 模块封装了这些底层差异。SelectorEventLoop 内部持有 selector 对象,调用其 select(timeout) 方法。
3.3 定时器实现:最小堆
除了IO事件,事件循环还需管理大量定时任务(如asyncio.sleep、call_later)。它使用一个最小堆(heapq)来排序所有定时器回调,每次循环开始时计算堆顶的到期时间,作为select的timeout参数。
# 简化的定时器添加
import heapq
timers = []
heapq.heappush(timers, (deadline, callback))
# 在 _run_once 中
now = loop.time()
if timers and timers[0][0] <= now:
# 执行所有到期的回调
while timers and timers[0][0] <= now:
deadline, cb = heapq.heappop(timers)
cb()
四、内置事件循环策略
4.1 策略(Policy)模式
asyncio允许在不同平台甚至同一平台下切换不同的事件循环实现。它通过策略(AbstractEventLoopPolicy)来管理:
- 获取当前事件循环(
get_event_loop()) - 设置新的事件循环(
set_event_loop()) - 创建新的事件循环(
new_event_loop())
默认策略 = WindowsSelectorEventLoopPolicy(Windows) 或 UnixSelectorEventLoopPolicy(Linux/macOS)。
4.2 三种主要事件循环实现
| 类名 | 适用平台 | 特点 |
|---|---|---|
SelectorEventLoop | 所有 | 基于selectors库,统一接口,但Windows下不支持管道等 |
ProactorEventLoop | Windows | 基于IOCP,性能更好,支持子进程 |
UVLoop | 跨平台 | 第三方加速库(uvloop),基于libuv(Node.js底层),性能提升2-4倍 |
如何选择:
- 一般无需手动选择,asyncio会根据平台自动选最优。
- 在Windows上如果想用SelectorEventLoop(例如为了兼容某些代码),可以手动设置策略。
- 生产环境Linux强烈推荐
uvloop:import uvloop; asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
4.3 获取/设置当前事件循环的规则
# 主线程
loop = asyncio.get_event_loop() # 如果当前线程没有,会创建一个新的事件循环(3.10+已弃用自动创建)
loop = asyncio.new_event_loop() # 显式创建
asyncio.set_event_loop(loop) # 设置为当前线程的循环
# 子线程中,不能用 get_event_loop()(会报错),需要显式创建或传递
重要:从Python 3.10开始,get_event_loop() 在没有事件循环时会抛出RuntimeError,而不是自动创建。推荐使用 asyncio.get_running_loop() 在协程内部获取当前循环。
五、源码逐行解析(核心片段)
我们以Python 3.11标准库中的 BaseEventLoop(Lib/asyncio/base_events.py)为例,提取最核心的调度代码。为便于理解,做了精简和注释。
5.1 主循环 run_forever
def run_forever(self):
"""运行事件循环直到 stop() 被调用"""
if self.is_running():
raise RuntimeError('Event loop is running')
self._thread_id = threading.get_ident()
self._running = True
try:
while True:
# 每次循环都要检查是否有人调用了 stop()
if self._stopping:
break
# 核心:执行单次tick,处理IO和定时器
self._run_once()
# 如果 stop() 被调用了,下一次循环会break
if self._stopping:
break
finally:
self._stopping = False
self._running = False
self._thread_id = None
5.2 单次tick _run_once
def _run_once(self):
"""执行一轮事件循环:处理就绪的IO和定时器"""
# 1. 获取当前时间
now = self.time()
# 2. 从定时器堆中获取下一个就绪的定时器时间,如果没有定时器,给一个默认值(例如100秒)
timeout = None
if self._scheduled:
# 堆顶是最小到期时间
when = self._scheduled[0]._when
timeout = max(0, when - now)
# 3. 调用 selector 等待 IO 事件,timeout 参数让它在超时后返回(即使没有IO事件)
event_list = self._selector.select(timeout)
now = self.time()
# 4. 处理IO事件:将就绪的文件描述符对应的回调加入就绪队列
for key, events in event_list:
# key 是 SelectorKey 对象,包含文件描述符和回调函数等
fd, callback, args = key.fileobj, key.data, key.args or ()
# 将回调加入 _ready 队列,等待执行
self._add_callback(callback, *args)
# 5. 处理到期的定时任务
while self._scheduled:
handle = self._scheduled[0]
if handle._when > now:
break
handle = heapq.heappop(self._scheduled)
# 定时回调直接加入 _ready 队列,准备执行
self._ready.append(handle)
# 6. 依次执行 _ready 队列中的所有回调
ntodo = len(self._ready)
for i in range(ntodo):
handle = self._ready.popleft()
if handle._cancelled:
continue
self._current_handle = handle
try:
handle._run() # 执行回调函数
except Exception as exc:
# 如果回调出错,通常交给异常处理(除非被抑制)
self.call_exception_handler({
'message': 'Unhandled exception in callback',
'exception': exc,
'handle': handle,
})
# 清空当前handle
self._current_handle = None
5.3 _add_callback 与 call_soon
def call_soon(self, callback, *args, context=None):
"""安排一个回调尽快被执行(在下一个tick)"""
handle = events.Handle(callback, args, self, context)
self._ready.append(handle) # 直接加入就绪队列,不需要等待IO
return handle
5.4 create_task 如何将协程注册到循环?
def create_task(self, coro, *, name=None, context=None):
"""将协程包装成 Task 对象并安排执行"""
task = tasks.Task(coro, loop=self, name=name, context=context)
# Task 构造函数内部会调用 loop.call_soon(task._step)
# 这样 Task 的第一步会在下一个tick被执行
return task
关键:任务创建后不会立即运行,而是通过call_soon把task._step(第一步)加入就绪队列,等待事件循环调度。
5.5 协程恢复的真正机制:Task._step
def _step(self, exc=None):
"""Task 的驱动方法,每次调用执行一步协程"""
try:
if exc is None:
# 正常恢复:调用协程的 send 方法继续执行
result = self._coro.send(None)
else:
# 抛出异常到协程内部
result = self._coro.throw(exc)
except StopIteration as exc:
# 协程完成,设置结果
self.set_result(exc.value)
return
except Exception as exc:
self.set_exception(exc)
return
# 如果 result 是 Future 或者协程,等待它
if isinstance(result, (asyncio.Future, Coroutine)):
result.add_done_callback(self._wakeup, context=self._context)
else:
# 普通值,不能 await,再次调用 _step 继续执行
self._loop.call_soon(self._step, None, context=self._context)
理解:每次await something,如果something是一个Future,当前Task会挂起,并把_wakeup注册到Future的add_done_callback上。当Future完成,会调用_wakeup,后者再调用_step恢复协程。
六、手动创建、启动、关闭事件循环
虽然asyncio.run()很方便,但某些高级场景需要手动控制循环生命周期。
6.1 基本流程
import asyncio
async def main():
print("Hello")
await asyncio.sleep(1)
print("World")
# 手动版本
loop = asyncio.new_event_loop() # 1. 创建新循环
asyncio.set_event_loop(loop) # 2. 设置为当前线程循环(可选)
try:
loop.run_until_complete(main()) # 3. 运行直到协程完成
# 或者 loop.run_forever(),需要手动调用 loop.stop()
finally:
loop.close() # 4. 关闭,释放资源
6.2 run_forever + stop 模式
适合需要持续运行直到某个条件触发的场景(如信号处理)。
import asyncio
async def periodic():
while True:
print("心跳")
await asyncio.sleep(2)
async def shutdown_after(seconds):
await asyncio.sleep(seconds)
loop = asyncio.get_running_loop()
loop.stop() # 停止事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 创建任务但不等待,让它们在后台运行
loop.create_task(periodic())
loop.create_task(shutdown_after(10))
print("事件循环启动,10秒后停止")
loop.run_forever() # 阻塞在这里,直到 loop.stop() 被调用
loop.close()
print("已停止")
6.3 关闭事件循环的注意事项
loop.close()后不能再执行任何操作,包括创建新任务。- 确保在关闭前取消了所有未完成任务,否则可能告警
Task was destroyed but it is pending!。 - 可以使用
asyncio.all_tasks(loop)获取所有任务并取消。
def safe_close(loop):
# 获取所有未完成任务
tasks = asyncio.all_tasks(loop)
for task in tasks:
task.cancel()
# 等待所有任务取消完成(可选)
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
loop.close()
七、事件循环常用API实战
7.1 创建与获取
| API | 说明 | 返回值 |
|---|---|---|
asyncio.new_event_loop() | 创建新的事件循环实例 | 事件循环对象 |
asyncio.get_event_loop() | 获取当前线程的事件循环(可能自动创建,但3.10+会报错) | 事件循环 |
asyncio.set_event_loop(loop) | 绑定当前线程到指定循环 | None |
asyncio.get_running_loop() | 在协程内部获取正在运行的循环(推荐) | 事件循环,否则报错 |
7.2 运行控制
| API | 说明 |
|---|---|
loop.run_until_complete(future) | 运行直到 future 完成,返回 future 的结果 |
loop.run_forever() | 永久运行,直到 loop.stop() 被调用 |
loop.stop() | 停止 run_forever(),但不是立即停止,会在下一次tick检查 |
loop.is_running() | 返回循环是否正在运行 |
loop.is_closed() | 返回循环是否已关闭 |
7.3 调度回调
| API | 说明 |
|---|---|
loop.call_soon(callback, *args) | 下一个tick立即执行回调 |
loop.call_later(delay, callback, *args) | 延迟 delay 秒后执行回调 |
loop.call_at(when, callback, *args) | 在绝对时间戳 when 执行回调 |
loop.time() | 返回当前时间(相对于循环的内部时钟,通常为 time.monotonic()) |
示例:
import asyncio
def regular_callback():
print("回调执行")
async def main():
loop = asyncio.get_running_loop()
loop.call_soon(regular_callback)
loop.call_later(2, regular_callback)
loop.call_at(loop.time() + 3, regular_callback)
await asyncio.sleep(4)
asyncio.run(main())
7.4 创建任务与Future
| API | 说明 |
|---|---|
loop.create_task(coro) | 从协程创建任务,等价于 asyncio.create_task |
loop.create_future() | 创建一个Future对象 |
7.5 线程/进程池集成
| API | 说明 |
|---|---|
loop.run_in_executor(executor, func, *args) | 在线程池或进程池中执行同步函数,返回一个可等待的Future |
(详见第13课)
八、多平台事件循环差异与坑点
8.1 Windows特有陷阱
SelectorEventLoop缺陷(Windows默认并不使用它,但曾作为默认):
- 不支持子进程(
asyncio.create_subprocess_exec会失败) - 不支持管道或某些socket特性
- 信号处理功能有限
解决方案:Python 3.8+ Windows上默认使用 ProactorEventLoop,支持子进程,性能更好。但ProactorEventLoop对于普通的asyncio.open_connection也是可用的。
如果需要手动切换:
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
8.2 Linux 与 macOS 的细微差异
- Linux的epoll是水平触发(LT),也可配置边缘触发(ET),asyncio默认使用LT。
- macOS的kqueue默认也类似LT,但实现略有不同,某些极端情况下文件描述符类型支持不如epoll完整(例如普通文件暂不支持)。
- 推荐生产环境使用Linux + uvloop,跨平台测试注意覆盖。
8.3 子线程无法运行事件循环
事件循环通常绑定到创建它的线程。在其他线程中调用 loop.call_soon_threadsafe 是安全的,但不能直接run_forever(会报错)。正确做法:
def worker(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
# 主线程
loop = asyncio.new_event_loop()
t = threading.Thread(target=worker, args=(loop,))
t.start()
或者使用 asyncio.run_coroutine_threadsafe(coro, loop) 从其他线程向事件循环提交任务。
九、开发中事件循环常见报错与解决方案
9.1 RuntimeError: Event loop is closed
常见场景:
- 在事件循环关闭后调用了
asyncio.get_event_loop()并尝试使用。 - 在
asyncio.run()之后,试图在同一个线程再次运行事件循环。
解决方案:
- 不要复用已关闭的循环,每次需要新循环时调用
asyncio.new_event_loop()。 - 如果使用
asyncio.run(),确保它只调用一次,且所有异步任务在其中完成。
# 错误示例
asyncio.run(main())
# 后续再调用 asyncio.get_event_loop().run_until_complete(...) # 报错
# 正确做法
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())
# 之后如果想再次运行,需要新创建循环
9.2 RuntimeError: There is no current event loop in thread
- 发生在子线程中未绑定事件循环时调用
asyncio.get_event_loop()。 - 解决方法:在子线程中显式创建并设置循环,或传递主线程的循环。
9.3 RuntimeError: Cannot run the event loop while another loop is running
- 嵌套调用
asyncio.run()或在协程内部调用run_until_complete。 - 解决方法:所有异步调用必须使用
await,不要嵌套启动事件循环。
9.4 DeprecationWarning: There is no current event loop
- 在非主线程调用
asyncio.get_event_loop(),且没有预先设置循环。 - 解决:使用
asyncio.new_event_loop()+set_event_loop()明确指定。
9.5 事件循环阻塞导致超时不准
import time
import asyncio
async def blocked_coro():
# 模拟长时间阻塞(错误做法)
time.sleep(5) # 同步阻塞
return
async def main():
try:
await asyncio.wait_for(blocked_coro(), timeout=1)
except asyncio.TimeoutError:
print("超时") # 实际上不会触发,因为阻塞根本不让事件循环运行
asyncio.run(main()) # 5秒后才返回,没有超时异常
原因:time.sleep(5) 阻塞了线程,事件循环没法运行超时的检查机制。
解决:永远不要在协程中使用同步阻塞调用;用 await asyncio.sleep() 或 loop.run_in_executor。
9.6 忘记保持任务引用导致任务被垃圾回收
async def background():
while True:
await asyncio.sleep(1)
print("后台运行")
async def main():
# ❌ 没有保存引用,任务可能被GC
asyncio.create_task(background())
await asyncio.sleep(2) # 打印1~2次后任务可能消失
# ✅ 保存引用
task = asyncio.create_task(background())
十、知识点总结
核心要点
- 事件循环定义:asyncio的调度核心,负责监控IO、定时器,调度协程/回调。
- 运行机制:
run_forever内部循环调用_run_once,每次 tick 包含:计算超时 → 等待IO → 处理到期的定时器 → 执行就绪回调。 - IO多路复用:底层使用
selectors模块,根据平台选择 epoll/kqueue/IOCP/select。 - 定时器:通过最小堆管理,计算出的最早到期时间用于
select超时。 - 任务调度:
create_task将协程包装为Task,首次通过call_soon入队;Task的_step方法通过协程的send恢复执行。 - 循环策略:不同平台自动选择最优实现;Linux推荐使用uvloop提速。
- API分层:
- 高级:
asyncio.run()一键启动 - 中级:
new_event_loop()+run_until_complete()手动控制 - 低级:
run_forever()+stop()精细控制
- 高级:
- 常见陷阱:事件循环已关闭、跨线程未设置、同步阻塞导致超时失效、任务引用丢失。
对比表:同步等待 vs 事件循环
| 角度 | 同步阻塞 | 事件循环 |
|---|---|---|
| 并发模型 | 多线程/多进程 | 单线程+协作式调度 |
| 上下文切换 | 操作系统调度,开销大 | 协程主动让出,开销小 |
| 等待IO方式 | 线程挂起,阻塞 | 注册回调,线程继续干别的 |
| 资源占用 | 每个任务栈MB级 | 每个协程几KB |
| 编程复杂度 | 较低(但锁麻烦) | 较高(异步传播) |
十一、课后实操题
题目1:手动实现简化版事件循环(不用asyncio)
请编写一个类 TinyEventLoop,满足:
add_task(coro):添加一个协程(生成器风格,使用yield来模拟等待,每次yield表示等待一段时间(秒))。run_until_complete():运行直到所有任务完成。- 内部使用一个就绪队列和时间轮询(可用
time.sleep模拟,但要求任务间能切换)。
示例用法:
def my_coro():
print("开始")
yield 1 # 等待1秒
print("一秒后")
yield 0.5
print("又过了0.5秒")
loop = TinyEventLoop()
loop.add_task(my_coro())
loop.run_until_complete()
目标:加深对事件循环任务调度的理解。
题目2:不同策略的事件循环
在Linux机器上(或WSL),分别使用默认策略和uvloop策略(需安装uvloop),运行以下代码,比较耗时差异:
import asyncio
async def task():
await asyncio.sleep(0.01)
async def main():
start = asyncio.get_running_loop().time()
await asyncio.gather(*[task() for _ in range(1000)])
end = asyncio.get_running_loop().time()
print(f"耗时 {end-start:.4f}秒")
# 先使用默认
asyncio.run(main())
# 再使用 uvloop(如果可用)
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
asyncio.run(main())
请记录输出并解释为什么uvloop更快。
题目3:跨线程提交任务
编写一个程序:
- 主线程启动一个事件循环,并运行一个永久的后台任务(每秒打印一次“心跳”)。
- 启动另一个工作线程,该线程5秒后通过
asyncio.run_coroutine_threadsafe向主线程的事件循环提交一个协程(打印“来自子线程”)。 - 主线程10秒后调用
loop.stop()停止循环。
要求正确处理线程安全,避免事件循环提前关闭。
题目4:事件循环源码阅读实践
从你的Python安装目录找到 Lib/asyncio/base_events.py,查找 _run_once 方法。回答:
_run_once中timeout参数是如何计算的?如果没有任何定时任务,timeout值是多少?selector.select(timeout)返回后,如何将就绪的IO事件转化成回调并放入_ready队列?- 执行
_ready队列中的回调过程中发生异常,事件循环是如何处理的?
题目5:调试一个错误的事件循环使用
分析以下代码为什么运行不到2秒就退出了,且没有打印“结束”,请修正:
import asyncio
async def main():
print("开始")
loop = asyncio.get_running_loop()
loop.stop()
print("结束")
asyncio.run(main())
修正后让程序正常打印“开始”和“结束”。
参考答案提示
-
题目1:需要维护一个任务列表,每个任务执行一步(
next(coro))得到等待时间,然后使用time.sleep模拟等待,但注意不能用sleep阻塞整个线程,需要用类似于“事件循环存储每个任务的下次唤醒时间”的方式,在run_until_complete中持续检查最小时间并sleep(差值)。 -
题目2:uvloop基于libuv,比标准库selectors更高效,减少系统调用和内存拷贝,并发性能提升明显。
-
题目3:关键在于子线程通过
asyncio.run_coroutine_threadsafe(coro, loop)获取Future,主线程事件循环需要持续运行(loop.run_forever())。 -
题目4:阅读源码得到详细答案(例如无定时器时
timeout为None,导致select无限等待;IO就绪时从selector获取key,key.data存储回调)。 -
题目5:
loop.stop()会停止事件循环,但此时main协程尚未完成,asyncio.run()会认为循环提前停止而报错。正确做法:不要在协程中随意调用stop(),如需停止应设计一个条件,或者使用return自然结束。
结语:本节课我们深入拆解了asyncio的发动机——事件循环。从轮询原理到源码分析,再到多平台差异和实战API,你应该已经建立起对事件循环的全面理解。下一课我们将正式进入 async/await 语法的零基础彻底吃透,届时你会发现:事件循环的知识会让语法理解变得轻而易举。
🔗《20节课 asyncio 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
2437

被折叠的 条评论
为什么被折叠?



