第2课:asyncio|事件循环EventLoop底层原理与核心源码解析

在这里插入图片描述

文章目录


开篇导读

学习目标

完成本节课后,你将能够:

  • 透彻理解事件循环在asyncio中的核心地位与运行机制
  • 掌握事件循环的轮询底层原理(select/poll/epoll/IOCP)
  • 明白不同操作系统下事件循环策略的差异与选择
  • 能够手动创建、启动、关闭事件循环,而不是只会asyncio.run()
  • 深入阅读asyncio事件循环的部分源码,理解关键调度流程
  • 解决开发中85%以上由事件循环引发的错误

适用人群

  • 已经能用asyncio写简单并发代码,但感觉内部像个黑盒
  • 面试中被问到“事件循环工作原理”时只能支支吾吾
  • 想彻底弄懂run_until_completerun_foreverstop()等API的底层行为
  • 希望在生产环境中排查事件循环相关故障(如“Event loop is closed”)

学完收益

  • 真正掌握异步编程的“发动机”——事件循环,不再只停留在API层面
  • 理解为什么某些写法会出错(如跨线程调用事件循环)
  • 具备自定义事件循环、集成第三方IO的能力基础
  • 遇到asyncio诡异报错时,知道从事件循环角度入手排查
  • 为后续阅读asyncio更底层源码(第17课)做好铺垫

前置知识:面向对象与Python基础

在深入之前,确保你已熟悉以下概念:

  • 类与继承:事件循环是一个抽象基类(AbstractEventLoop),具体实现在不同平台有不同的子类。
  • 生成器与迭代器:早期asyncio基于yield from,现代虽然用async/await,但底层仍依赖生成器的sendthrow
  • 文件描述符(fd):Linux/Unix下一切皆文件,socket、管道、标准输入输出都有对应的整数fd。事件循环本质是监听这些fd上的事件。
  • 回调函数:事件循环大量使用回调,理解回调的执行时机很重要。

推荐提前阅读

  • selectors模块:标准库中IO多路复用的高级封装。
  • concurrent.futures 中的 Future:asyncio的Future类似,但专为事件循环设计。

一、事件循环是什么?核心作用

1.1 定义

事件循环(Event Loop) 是asyncio的核心调度器。它负责:

  1. 注册 协程、任务、回调。
  2. 监控 IO事件(socket可读/可写)、定时器(sleep到期)。
  3. 分发 事件:当某个事件就绪时,调用对应的回调或恢复协程执行。
  4. 管理 任务的创建、取消、异常传播。

通俗比喻:事件循环就像一个超级耐心的餐厅经理,他手里有一堆订单(协程任务)。他不断扫视厨房(操作系统内核)各个出菜口(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_foreverrun_until_complete 被调用)。
  • STOPPINGstop() 被调用,正在优雅关闭。
  • CLOSEDclose() 被调用,资源释放。
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事件驱动,只返回就绪fdO(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.sleepcall_later)。它使用一个最小堆heapq)来排序所有定时器回调,每次循环开始时计算堆顶的到期时间,作为selecttimeout参数。

# 简化的定时器添加
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下不支持管道等
ProactorEventLoopWindows基于IOCP,性能更好,支持子进程
UVLoop跨平台第三方加速库(uvloop),基于libuv(Node.js底层),性能提升2-4倍

如何选择

  • 一般无需手动选择,asyncio会根据平台自动选最优。
  • 在Windows上如果想用SelectorEventLoop(例如为了兼容某些代码),可以手动设置策略。
  • 生产环境Linux强烈推荐 uvloopimport 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标准库中的 BaseEventLoopLib/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_callbackcall_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_soontask._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())

十、知识点总结

核心要点

  1. 事件循环定义:asyncio的调度核心,负责监控IO、定时器,调度协程/回调。
  2. 运行机制run_forever 内部循环调用 _run_once,每次 tick 包含:计算超时 → 等待IO → 处理到期的定时器 → 执行就绪回调。
  3. IO多路复用:底层使用 selectors 模块,根据平台选择 epoll/kqueue/IOCP/select。
  4. 定时器:通过最小堆管理,计算出的最早到期时间用于 select 超时。
  5. 任务调度create_task 将协程包装为Task,首次通过 call_soon 入队;Task的_step方法通过协程的send恢复执行。
  6. 循环策略:不同平台自动选择最优实现;Linux推荐使用uvloop提速。
  7. API分层
    • 高级:asyncio.run() 一键启动
    • 中级:new_event_loop() + run_until_complete() 手动控制
    • 低级:run_forever() + stop() 精细控制
  8. 常见陷阱:事件循环已关闭、跨线程未设置、同步阻塞导致超时失效、任务引用丢失。

对比表:同步等待 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 方法。回答:

  1. _run_oncetimeout 参数是如何计算的?如果没有任何定时任务,timeout 值是多少?
  2. selector.select(timeout) 返回后,如何将就绪的IO事件转化成回调并放入 _ready 队列?
  3. 执行 _ready 队列中的回调过程中发生异常,事件循环是如何处理的?

题目5:调试一个错误的事件循环使用

分析以下代码为什么运行不到2秒就退出了,且没有打印“结束”,请修正:

import asyncio

async def main():
    print("开始")
    loop = asyncio.get_running_loop()
    loop.stop()
    print("结束")

asyncio.run(main())

修正后让程序正常打印“开始”和“结束”。


参考答案提示

  1. 题目1:需要维护一个任务列表,每个任务执行一步(next(coro))得到等待时间,然后使用time.sleep模拟等待,但注意不能用sleep阻塞整个线程,需要用类似于“事件循环存储每个任务的下次唤醒时间”的方式,在run_until_complete中持续检查最小时间并sleep(差值)

  2. 题目2:uvloop基于libuv,比标准库selectors更高效,减少系统调用和内存拷贝,并发性能提升明显。

  3. 题目3:关键在于子线程通过asyncio.run_coroutine_threadsafe(coro, loop)获取Future,主线程事件循环需要持续运行(loop.run_forever())。

  4. 题目4:阅读源码得到详细答案(例如无定时器时timeoutNone,导致select无限等待;IO就绪时从selector获取key,key.data存储回调)。

  5. 题目5loop.stop() 会停止事件循环,但此时 main 协程尚未完成,asyncio.run() 会认为循环提前停止而报错。正确做法:不要在协程中随意调用stop(),如需停止应设计一个条件,或者使用return自然结束。

结语:本节课我们深入拆解了asyncio的发动机——事件循环。从轮询原理到源码分析,再到多平台差异和实战API,你应该已经建立起对事件循环的全面理解。下一课我们将正式进入 async/await 语法的零基础彻底吃透,届时你会发现:事件循环的知识会让语法理解变得轻而易举。


🔗《20节课 asyncio 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值