前言
协程(coroutine)是在同一个线程内做并发 的方式:由程序自己决定「在哪里让出控制权」,切换发生在用户态,成本远低于线程切换。适合大量 IO 等待 的场景(网络请求、数据库、文件);对 CPU 密集任务没有提速效果(见《进程》《线程》里的 GIL 讨论)。
本篇的代码全部在本机跑过,环境是 Python 3.14.10(Windows) ,第三方库 greenlet 3.3.2、gevent 26.9.0。因为协程的调度顺序、耗时都依赖运行环境,所以我把所有输出都改成了可复现的形式 :用列表记录交错顺序、用布尔值表示「是否并发」,不打印具体秒数。
并发模型的三种选择
模型
切换由谁决定
并行还是并发
适合
代价
多进程
操作系统
真并行(多核)
CPU 密集
进程重、通信要序列化
多线程
操作系统
并发(受 GIL 限制,IO 时能并行等待)
阻塞式 IO、调用 C 库
栈内存、锁、竞态
协程
程序自己 (遇到 await/switch 才让出)
并发(单线程)
海量 IO 等待(成千上万连接)
库要配合(异步 API 或猴子补丁)、一处阻塞全卡
一句话记法:CPU 密集找进程,阻塞 IO 找线程,海量等待找协程。
从生成器说起
yield 让函数能在中间暂停并把控制权交出去 ,这就是协程的机制原型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def countdown (name, n ): while n > 0 : n -= 1 yield f'{name} 还剩 {n} ' if __name__ == '__main__' : a = countdown('A' , 2 ) b = countdown('B' , 3 ) while True : try : print (next (a)) print (next (b)) except StopIteration: break
1 2 3 4 A 还剩 1 B 还剩 2 A 还剩 0 B 还剩 1
输出只有 4 行不是 5 行:A 的 range(2) 先耗尽,抛 StopIteration 后被 except 接住退出循环,B 还剩最后一轮没跑完 。用生成器手写调度器很容易漏掉这种「谁先结束」的问题。
现代写法不该再用 try/except StopIteration 手搓轮询:要么 itertools/select,要么直接上 asyncio。
生成器和协程在 API 层面是两条血脉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import asyncioimport inspectdef plain (): yield 1 async def coro (): await asyncio.sleep(0 ) return 'ok' async def main (): print ('生成器函数:' , inspect.isgeneratorfunction(plain), '| 协程函数:' , inspect.iscoroutinefunction(coro)) c = coro() print ('调用协程函数得到的是:' , type (c).__name__, '| 可 await:' , inspect.isawaitable(c)) print ('await 它的结果:' , await c) print ('生成器能不能 await:' , inspect.isawaitable(plain())) asyncio.run(main())
1 2 3 4 生成器函数: True | 协程函数: True 调用协程函数得到的是: coroutine | 可 await: True await 它的结果: ok 生成器能不能 await: False
关键结论:协程对象可以 await,生成器对象不行 。所以 for x in gen() 与 async for x in agen() 是两套协议(__iter__/__next__ vs __aiter__/__anext__)。
greenlet:把切换交给代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from greenlet import greenletlog = [] def a (): for i in range (3 ): log.append(f'A{i} ' ) gb.switch() def b (): for i in range (3 ): log.append(f'B{i} ' ) gc.switch() def c (): for i in range (3 ): log.append(f'C{i} ' ) ga.switch() if __name__ == '__main__' : ga = greenlet(a) gb = greenlet(b) gc = greenlet(c) ga.switch() print ('切换轨迹:' , log) print ('是否已经跑完(dead):' , [ga.dead, gb.dead, gc.dead])
1 2 切换轨迹: ['A0', 'B0', 'C0', 'A1', 'B1', 'C1', 'A2', 'B2', 'C2'] 是否已经跑完(dead): [True, False, False]
输出严格按 A0 B0 C0 A1 B1 C1 A2 B2 C2 —— 顺序是代码里 switch() 写出来的 ,不是调度器决定的。这就是 greenlet 的定位:机制清晰、但没有任何「自动」可言。
最后一行 [True, False, False] 说明:A 从循环里 return 之后,B、C 还停在半路 上(dead=False)。协程对象生命周期要自己收,别让它们悬着。
switch() 还能双向传值 ,这是 greenlet 相对裸生成器多出来的能力:
1 2 3 4 5 6 7 8 9 10 11 12 13 from greenlet import greenletdef child (): got = greenlet.getcurrent().parent.switch('child 先给父一个值' ) return f'child 收到: {got} ' if __name__ == '__main__' : g = greenlet(child) first = g.switch() print ('第一次 switch 拿到:' , first) second = g.switch('喂给 child 的值' ) print ('第二次 switch 拿到:' , second) print ('child 已结束:' , g.dead)
1 2 3 第一次 switch 拿到: child 先给父一个值 第二次 switch 拿到: child 收到: 喂给 child 的值 child 已结束: True
第一次 g.switch() 进入子 greenlet,执行到 parent.switch(值) 时把值交回主线;
第二次 g.switch(值) 时,这个入参就是子 greenlet 里那个 switch() 表达式的返回值。
装法:pip install greenlet(本机 3.3.2)。典型用途是自己写调度器、或者给库做底层协程支持——gevent 就是它上面的一个封装。
gevent:用猴子补丁自动让路
greenlet 要手写切换太累。gevent 的思路是:把标准库里那些会阻塞的调用换成「会让路」的版本 ,于是普通同步代码也能并发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import timeimport geventfrom gevent import monkeymonkey.patch_all() seen = [] def job (name, ticks ): for i in range (ticks): seen.append(f'{name} {i} ' ) time.sleep(0.05 ) if __name__ == '__main__' : started = time.perf_counter() gevent.joinall([gevent.spawn(job, 'A' , 3 ), gevent.spawn(job, 'B' , 3 )]) elapsed = time.perf_counter() - started print ('交错执行:' , seen) print (f'6 次 0.05 秒的等待是否并发(总耗时 < 0.3 秒): {elapsed < 0.3 } ' ) print ('time.sleep 已被替换成:' , time.sleep.__module__)
1 2 3 交错执行: ['A0', 'B0', 'A1', 'B1', 'A2', 'B2'] 6 次 0.05 秒的等待是否并发(总耗时 < 0.3 秒): True time.sleep 已被替换成: gevent.hub
A0 B0 A1 B1 A2 B2 是交错 的:每次 time.sleep 都让另一个 greenlet 上场。
elapsed < 0.3 为 True:6 次 0.05 秒的等待只花了约 0.15 秒——并发成立。
time.sleep.__module__ 读回来是 gevent.hub :这就是猴子补丁生效的直接证据。
三条必须记住的规则
1 2 3 4 5 6 from gevent import monkeymonkey.patch_all()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import geventfrom gevent import monkeyimport urllib.requestmonkey.patch_all() def download (url ): content = urllib.request.urlopen(url).read() print (f'下载完成 {url} ,字节数:{len (content)} ' ) if __name__ == '__main__' : urls = ['https://www.example.com/' , 'https://www.python.org/' ] gevent.joinall([gevent.spawn(download, u) for u in urls])
gevent.spawn 返回 Greenlet,joinall 等全部结束;pool = gevent.pool.Pool(10) 可以限并发数(爬虫必备)。
装法:pip install gevent(本机 26.9.0,底层是 libev + greenlet)。
asyncio:标准库这条主线
asyncio 是官方 的异步 I/O 框架:事件循环 就是单线程里那个「谁准备好了就跑去执行一会儿、执行到 await 就交还控制权」的调度器。一个事件循环 + 一堆任务,任务在 await 处挂起,循环切去跑别的任务。现代 Python 网络编程(FastAPI、httpx、redis-py、SQLAlchemy 异步引擎)都走这条路。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import asyncioasync def coro (x ): await asyncio.sleep(0 ) return x + 1 async def main (): print ('当前循环实现:' , type (asyncio.get_running_loop()).__name__) print ('直接 await:' , await coro(1 )) t = asyncio.create_task(coro(2 )) print ('任务类型:' , type (t).__name__, '| 是 Future 的子类:' , isinstance (t, asyncio.Future)) print ('还没 await 时 done:' , t.done()) print ('await 之后:' , await t, '| done:' , t.done()) print ('当前有没有正在跑的任务:' , asyncio.current_task() is not None ) asyncio.run(main()) print ('run 结束后再取运行中的循环:' , end=' ' )try : asyncio.get_running_loop() print ('拿到了(不应该)' ) except RuntimeError as e: print ('RuntimeError:' , e.args[0 ])
1 2 3 4 5 6 7 当前循环实现: ProactorEventLoop 直接 await: 2 任务类型: Task | 是 Future 的子类: True 还没 await 时 done: False await 之后: 3 | done: True 当前有没有正在跑的任务: True run 结束后再取运行中的循环: RuntimeError: no running event loop
要点:
asyncio.run(main()) 是唯一推荐的入口 :建新循环 → 跑完协程 → 收尾关闭。之后在线程里再取 get_running_loop() 会抛 RuntimeError: no running event loop(实测)。
Windows 上的默认实现是 ProactorEventLoop (实测;Linux 是 SelectorEventLoop)。以前 Windows 用 Selector 时不能开子进程,Proactor 解决了这个。写跨平台库时要清楚这一点。
asyncio.Task 是 asyncio.Future 的子类(实测 isinstance(t, asyncio.Future) 为 True):Future = 「一个还没有的结果」,Task = 「跑一个协程的 Future」 。
create_task() 之后任务就已经排进循环了,await 只是取结果;不调 await 也不会「没跑」,但不持有引用的裸任务可能被垃圾回收 ,这是经典事故(用 TaskGroup 或把任务存进集合)。
一个必须建立的直觉:await 才是让出点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import asyncioasync def say (who, delay ): print (f'{who} 开始' ) await asyncio.sleep(delay) print (f'{who} 结束' ) return who async def main (): print ('顺序 await(一个跑完才跑下一个)' ) await say('A' , 0.05 ) await say('B' , 0.05 ) print ('用 create_task 并发' ) t1 = asyncio.create_task(say('A' , 0.05 )) t2 = asyncio.create_task(say('B' , 0.05 )) print ('两个任务都还活着:' , not t1.done() and not t2.done()) done = await t1 print ('第一个完成:' , done) await t2 asyncio.run(main())
1 2 3 4 5 6 7 8 9 10 11 12 顺序 await(一个跑完才跑下一个) A 开始 A 结束 B 开始 B 结束 用 create_task 并发 两个任务都还活着: True A 开始 B 开始 A 结束 B 结束 第一个完成: A
前半段 await 逐个串行:A 完整跑完才开始 B。
后半段先 create_task 两个任务,于是 A 开始 / B 开始 连着出现,并发 了;两个任务都还活着: True 说明「创建后立刻执行了一段」而不是排队等待。
结论:写 await f() 是串行;写 t = create_task(f()) 再 await t 才是并发。 这是 asyncio 最容易写错的地方。
批量并发:gather 与 TaskGroup
asyncio.gather(老而通用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import asyncioimport timeasync def worker (n, delay ): await asyncio.sleep(delay) return f'任务{n} 完成' async def boom (): raise ValueError('出错了' ) async def main (): start = time.perf_counter() results = await asyncio.gather(*(worker(i, 0.1 ) for i in range (5 ))) elapsed = time.perf_counter() - start print ('结果按提交顺序返回:' , results) print (f'5 个 0.1 秒的任务并发跑完(耗时 < 0.2 秒): {elapsed < 0.2 } ' ) print ('耗时确实等过(> 0.1 秒):' , elapsed > 0.1 ) ok, err = await asyncio.gather(worker(1 , 0.01 ), boom(), return_exceptions=True ) print ('return_exceptions=True 时第二个是异常对象:' , isinstance (err, BaseException), type (err).__name__) asyncio.run(main())
1 2 3 4 结果按提交顺序返回: ['任务0完成', '任务1完成', '任务2完成', '任务3完成', '任务4完成'] 5 个 0.1 秒的任务并发跑完(耗时 < 0.2 秒): True 耗时确实等过(> 0.1 秒): True return_exceptions=True 时第二个是异常对象: True ValueError
gather 的返回值按提交顺序 排列,不是完成顺序(实测 ['任务0完成', …, '任务4完成'])。要按完成顺序处理就 asyncio.as_completed()。
默认行为是第一个异常立刻向上抛 ,其它任务仍在跑(容易被忽略)。加 return_exceptions=True 才会把异常当结果收集回来(实测拿到 ValueError 实例)。
asyncio.TaskGroup(3.11+,推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import asyncioasync def ok (name ): await asyncio.sleep(0.02 ) return name async def bad (name ): await asyncio.sleep(0.05 ) raise RuntimeError(f'{name} 失败' ) async def main (): try : async with asyncio.TaskGroup() as tg: tg.create_task(ok('A' )) tg.create_task(bad('B' )) except * RuntimeError as eg: print ('捕获到子任务异常:' , eg.exceptions[0 ].args[0 ], '| 异常条数:' , len (eg.exceptions)) async with asyncio.TaskGroup() as tg: t = tg.create_task(ok('C' )) print ('退出 with 之后任务已完成,结果:' , t.result()) print ('TaskGroup 要求同一任务内使用,跨任务 add_task 会报错:' , hasattr (asyncio.TaskGroup, 'create_task' )) asyncio.run(main())
1 2 3 捕获到子任务异常: B 失败 | 异常条数: 1 退出 with 之后任务已完成,结果: C TaskGroup 要求同一任务内使用,跨任务 add_task 会报错: True
TaskGroup 是结构化并发 :退出 async with 时保证所有子任务都已结束;任何一个失败,其余的会被取消 ,异常打包成 ExceptionGroup 抛出,用 except* 按类型分拣(实测能拿到 eg.exceptions[0])。
对比:gather 是「把一堆协程当参数」,TaskGroup 是「在一个作用域里随时 create_task」,后者更容易写正确的取消逻辑,也更容易看清任务边界。
记一句:新代码优先 async with asyncio.TaskGroup() as tg: 。
超时与取消
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import asyncioasync def slow (): await asyncio.sleep(1 ) return '不该等到这里' async def main (): try : async with asyncio.timeout(0.1 ): print (await slow()) except TimeoutError as e: print ('超时:' , type (e).__name__, '| 消息为空:' , e.args == ()) try : await asyncio.wait_for(slow(), timeout=0.1 ) except TimeoutError: print ('wait_for 超时同样抛 TimeoutError' ) print ('asyncio.TimeoutError 与内置 TimeoutError 是同一个类:' , asyncio.TimeoutError is TimeoutError) asyncio.run(main())
1 2 3 超时: TimeoutError | 消息为空: True wait_for 超时同样抛 TimeoutError asyncio.TimeoutError 与内置 TimeoutError 是同一个类: True
3.11+ 用 async with asyncio.timeout(0.1):,作用域内所有 await 共享这个预算;老写法 await asyncio.wait_for(coro(), 0.1) 仍然有效。
超时抛的就是内置 TimeoutError (实测 asyncio.TimeoutError is TimeoutError 为 True,3.11 起二者合一)。别在 except 里只抓 asyncio.TimeoutError 然后困惑于漏捕。
取消的注意点:
1 2 3 4 5 6 7 8 9 10 11 12 13 async def worker (): try : await asyncio.sleep(10 ) except asyncio.CancelledError: cleanup() raise async def main (): t = asyncio.create_task(worker()) await asyncio.sleep(0.05 ) t.cancel() await asyncio.gather(t, return_exceptions=True ) print (t.cancelled())
wait_for 超时时会取消 被等的任务;asyncio.timeout 同理。清理逻辑写在 except CancelledError: … raise 里。
Future:一个还没兑现的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import asyncioasync def main (): loop = asyncio.get_running_loop() fut = loop.create_future() print ('刚创建时 done:' , fut.done(), '| cancelled:' , fut.cancelled()) async def deliver (): await asyncio.sleep(0.02 ) fut.set_result('递过来的值' ) task = asyncio.create_task(deliver()) value = await fut print ('等待 future 拿到:' , value) await task fut2 = loop.create_future() fut2.set_exception(ValueError('future 也能装异常' )) try : await fut2 except ValueError as e: print ('抛出的是:' , e.args[0 ]) fut3 = loop.create_future() fut3.cancel() print ('取消后 done:' , fut3.done(), '| cancelled:' , fut3.cancelled()) asyncio.run(main())
1 2 3 4 刚创建时 done: False | cancelled: False 等待 future 拿到: 递过来的值 抛出的是: future 也能装异常 取消后 done: True | cancelled: True
loop.create_future() 造一个「空壳结果」,谁先算出来谁 set_result / set_exception,别的地方 await 它。这是把回调式 API 桥接成协程 的标准手段:
1 2 3 4 async def wait_callback_api (loop ): fut = loop.create_future() some_lib.run(callback=lambda value: loop.call_soon_threadsafe(fut.set_result, value)) return await fut
cancelled() 只在真的被取消过时为 True,别拿它当「有没有结果」用(实测刚创建时 done 与 cancelled 都是 False)。
协程之间的协作:队列与同步原语
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import asyncioasync def producer (q, n ): for i in range (n): await q.put(i) await q.put(None ) async def consumer (q, out ): while True : item = await q.get() if item is None : break out.append(item) q.task_done() async def counter (lock, state ): for _ in range (1000 ): async with lock: state[0 ] += 1 async def main (): q = asyncio.Queue(maxsize=3 ) out = [] await asyncio.gather(producer(q, 3 ), consumer(q, out)) print ('队列传了:' , out, '| 已空:' , q.empty(), '| qsize:' , q.qsize()) lock = asyncio.Lock() state = [0 ] async with asyncio.TaskGroup() as tg: for _ in range (5 ): tg.create_task(counter(lock, state)) print ('加锁后计数:' , state[0 ], '| 锁是否仍被占用:' , lock.locked()) ev = asyncio.Event() seen = [] async def waiter (): await ev.wait() seen.append('醒来' ) w = asyncio.create_task(waiter()) await asyncio.sleep(0.02 ) print ('事件未设置时等待者没醒:' , seen == []) ev.set () await w print ('设置后:' , seen, '| is_set:' , ev.is_set()) asyncio.run(main())
1 2 3 4 队列传了: [0, 1, 2] | 已空: True | qsize: 0 加锁后计数: 5000 | 锁是否仍被占用: False 事件未设置时等待者没醒: True 设置后: ['醒来'] | is_set: True
asyncio.Queue :生产者-消费者的标准件。给 maxsize 才能形成背压(生产者会阻塞,不会吃爆内存);task_done() 与 join() 配对用来判断「全部处理完」。实测取空后 empty() 为 True、qsize() 为 0。
asyncio.Lock :协程里不要 用 threading.Lock(拿不到锁时会阻塞整个线程 = 卡死事件循环)。实测 5 个协程各 +1 一千次,加锁后精确得到 5000;去掉锁的话结果会随机偏小。
asyncio.Event :一次性通知(set 后所有 wait() 者都醒);实测未 set 前等待者确实没醒。
asyncio.Semaphore :限并发,爬虫里最常用:
1 2 3 4 5 sem = asyncio.Semaphore(10 ) async def fetch (session, url ): async with sem: return await (await session.get(url)).text()
asyncio.Condition :等待某个状态成立才继续。
异步迭代器与异步上下文管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import asyncioclass Range : def __init__ (self, n ): self .n = n def __aiter__ (self ): return self async def __anext__ (self ): if self .n <= 0 : raise StopAsyncIteration self .n -= 1 await asyncio.sleep(0 ) return self .n + 1 async def main (): got = [v async for v in Range(3 )] print ('异步迭代:' , got) async for v in Range(2 ): print ('逐个拿到:' , v) asyncio.run(main())
1 2 3 异步迭代: [3, 2, 1] 逐个拿到: 2 逐个拿到: 1
__aiter__ 必须返回迭代器自身 (在 3.5/3.6 时代它曾允许是个协程,后来废了);__anext__ 是协程,靠 StopAsyncIteration 结束。
async for 常见于:分页 API 游标、数据库游标、aiohttp 的响应体分块、readlines 风格的异步流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import asynciolog = [] class Resource : def __init__ (self, name ): self .name = name async def __aenter__ (self ): await asyncio.sleep(0 ) log.append(f'打开 {self.name} ' ) return self async def __aexit__ (self, exc_type, exc, tb ): log.append(f'关闭 {self.name} ' ) return False async def main (): async with Resource('连接池' ) as r: log.append('使用 ' + r.name) print ('轨迹:' , log) try : async with Resource('文件' ): raise ValueError('里面炸了' ) except ValueError: print ('异常没被吞,最后一行是:' , log[-1 ]) asyncio.run(main())
1 2 轨迹: ['打开 连接池', '使用 连接池', '关闭 连接池'] 异常没被吞,最后一行是: 关闭 文件
async with 允许进入/退出时做 await(异步连接池、异步文件锁、事务提交/回滚)。
__aexit__ 返回真值才吞异常(实测返回 False 时外层 except ValueError 正常拿到)。写库时别不小心吞掉异常。
标准库里已经有一堆异步上下文管理器:aiohttp.ClientSession()、asyncio.StreamWriter(配合 wait_closed)、async with anyio.create_task_group()。
绕不开的阻塞代码
asyncio 最怕「一个阻塞调用卡住整个循环」。三种解法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import asyncioimport timefrom concurrent.futures import ThreadPoolExecutordef blocking (n ): time.sleep(0.05 ) return n * n async def main (): loop = asyncio.get_running_loop() start = time.perf_counter() results = await asyncio.gather(*(loop.run_in_executor(None , blocking, i) for i in range (3 ))) elapsed = time.perf_counter() - start print ('放到线程池里跑的同步结果:' , results) print (f'3 个 0.05 秒的阻塞调用并发完成(< 0.1 秒): {elapsed < 0.1 } ' ) with ThreadPoolExecutor(max_workers=2 ) as pool: futures = [asyncio.wrap_future(pool.submit(blocking, i)) for i in range (2 )] done, pending = await asyncio.wait(futures, return_when=asyncio.ALL_COMPLETED) print ('wrap_future 接进事件循环:' , sorted (f.result() for f in done)) print ('done 数量:' , len (done), '| pending 数量:' , len (pending)) asyncio.run(main())
1 2 3 4 放到线程池里跑的同步结果: [0, 1, 4] 3 个 0.05 秒的阻塞调用并发完成(< 0.1 秒): True wrap_future 接进事件循环: [0, 1] done 数量: 2 | pending 数量: 0
做法
写法
用在哪
线程池托管
await loop.run_in_executor(None, blocking, arg)
只有同步版本的库(老驱动、requests、PIL)
桥接 concurrent.futures
asyncio.wrap_future(fs) / asyncio.ensure_future(fs)
已经有自己的线程池,想在协程里 await
3.9+ 的简便写法
await asyncio.to_thread(blocking, arg)
单个阻塞调用,最常用
进程池
loop.run_in_executor(ProcessPoolExecutor(), cpu_bound)
CPU 密集,绕开 GIL
默认线程池大小:min(32, os.cpu_count() + 4)(3.8+),别把几百个阻塞任务全压给它。
asyncio.sleep 与 time.sleep 的区别要盯死 :在协程里误写 time.sleep(1),整个循环停 1 秒——这是最常见的性能事故。
网络:asyncio 自己的流 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import asyncioasync def handle (reader, writer ): data = await reader.readline() writer.write(b'echo:' + data) await writer.drain() writer.close() async def main (): server = await asyncio.start_server(handle, '127.0.0.1' , 0 ) port = server.sockets[0 ].getsockname()[1 ] async with server: reader, writer = await asyncio.open_connection('127.0.0.1' , port) writer.write(b'hello\n' ) await writer.drain() print ('服务端回:' , await reader.readline()) writer.close() await writer.wait_closed() print ('监听 socket 的协议族:' , server.sockets[0 ].family.name) asyncio.run(main())
1 2 服务端回: b'echo:hello\n' 监听 socket 的协议族: AF_INET
asyncio.start_server / open_connection 给的是 Streams (StreamReader/StreamWriter),比裸 socket 友好:readline、readexactly、writelines、drain。
端口传 0 让系统分配,server.sockets[0].getsockname()[1] 读回来(实测 AF_INET,与《网络编程》里的写法一致)。
writer.close() 之后要 await writer.wait_closed(),否则最后几个字节可能还没发完。
HTTP 层不要自己拿 asyncio 写,用 aiohttp (客户端/服务端都行)或 httpx.AsyncClient ;DNS 解析用 loop.getaddrinfo(别用同步的 socket.getaddrinfo)。
异步数据库与缓存(写法备忘)
本机没有 MySQL/Redis 服务,这一节的代码未实测 ,只记录现代正确的包选择与骨架,避免照抄老教程。
1 2 3 4 5 6 7 8 import redis.asyncio as redisasync def main (): r = redis.Redis(host='127.0.0.1' , port=6379 , decode_responses=True ) async with r: await r.set ('k' , 'v' ) print (await r.get('k' ))
1 2 3 4 5 6 7 8 9 10 11 12 import aiomysqlasync def main (): pool = await aiomysql.create_pool(host='127.0.0.1' , port=3306 , user='u' , password='p' , db='demo' , autocommit=True ) async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute('SELECT 1' ) print (await cur.fetchone()) pool.close() await pool.wait_closed()
1 2 3 4 from sqlalchemy.ext.asyncio import create_async_engineengine = create_async_engine('sqlite+aiosqlite:///demo.db' )
async with 的粒度决定了连接归还时机,别让连接活过事务。
迁移口诀:同一个事件循环里不要用同步驱动 (pymysql、redis-py 同步客户端会阻塞循环)。
uvloop:把事件循环换成 libuv
uvloop 是 asyncio 事件循环的高性能替换(基于 libuv,Node.js 的底层),常见于高并发服务端:
1 2 3 4 5 6 7 import asyncioimport uvloopuvloop.install() asyncio.run(main())
Windows 不支持 uvloop (Linux/macOS 专用)。本机实测的循环实现是 ProactorEventLoop,正是「没用上 uvloop」的样子;Policy 相关 API 在 3.14 已带 DeprecationWarning,改用 loop_factory。
收益主要在大量连接/吞吐场景(常见说法是快 2~4 倍),业务代码不用改,但别指望它救阻塞调用 。
常见坑速查
定义了 async def 却忘了调用/await → 协程根本没跑,控制台出现 coroutine 'x' was never awaited 警告。
在协程里调 time.sleep/同步 requests/同步驱动 → 整个事件循环卡住,并发形同虚设。改 await asyncio.sleep、asyncio.to_thread 或异步库。
await 一堆任务是串行 → 要并发就 create_task 或 gather(实测对比见上)。
gather 里一个任务抛异常就以为全都停了 → 默认是异常立刻上抛,但其他任务仍在跑;用 TaskGroup 才有「一荣俱荣一损俱损」的取消语义。
except Exception 接不住 CancelledError → 它继承自 BaseException;接住了也别忘记 raise,否则取消不掉、asyncio.run 收尾卡住。
裸 create_task 不保存引用 → 任务可能被 GC 掉。TaskGroup 或 tasks.add(t) 保住。
threading.Lock / queue.Queue 用在协程里 → 会阻塞线程,用 asyncio.Lock / asyncio.Queue。
asyncio.Queue 不设 maxsize → 生产者快过消费者,内存一路涨。
在已运行的循环里再调 asyncio.run() → 抛 RuntimeError(同一线程只能有一个运行中的循环)。Jupyter/交互环境直接 await 即可。
get_event_loop() 的旧习惯 → 3.12 起在没有运行循环时调用会告警,3.14 已见 DeprecationWarning;新代码只用 get_running_loop()(协程内)或 asyncio.run()(入口)。
gevent 补丁打晚了 → 之前 import 的模块还拿着原来的阻塞函数,看起来「补丁没生效」。patch_all() 必须在最前。
协程数量爆炸 → 每个连接开一个协程虽然便宜,但上万时仍要限并发(Semaphore / Pool)。
练习
把《线程》里那个「5 个线程各加 1000 次」的例子改写成 asyncio.Lock 版本,验证计数恒为 5000;再把锁去掉,观察偏小的结果。
用 TaskGroup + Semaphore(5) 并发抓 20 个 URL(可用本地 python -m http.server 当靶子),要求:单个失败不影响其它、整体有超时、结果按 URL 顺序汇总。
把一段 requests 同步下载改造成三种写法:asyncio.to_thread、run_in_executor、aiohttp,对比代码量与吞吐。
实现一个自定义异步迭代器(分页游标:每页 await 一次网络),用 async for 消费。
实现 async with 版的数据库事务(__aenter__ 开事务、__aexit__ 按有无异常提交或回滚),用 aiosqlite 或干脆用假连接验证轨迹。
分别用 gather 和 TaskGroup 写「一个子任务失败」的场景,观察未捕获异常与任务取消的差异。
参考资料