一、协程是什么
死板定义必背,但不好理解:
- 协程是一种可以被挂起,并且在挂起之后,还可以恢复执行的函数。
- 协程又称微线程,是一种用户态内的上下文切换技术。
(1)为什么需要协程
我们需要协程,核心原因是它能以极低的资源开销高效处理高并发场景,尤其擅长应对网络请求、文件读写等I/O密集型任务。弥补了传统线程模型的不足,具体优势如下:
- 避免线程切换的高成本:线程由操作系统调度,频繁切换需保存 / 恢复上下文,开销大;协程由程序(事件循环)主动调度,切换仅在用户态 "挂起 - 恢复",成本近乎零,单线程可以轻松管理数万协程。
- 最大化 CPU 利用率:传统单线程遇 IO 等待(如网络请求)会闲置 CPU;协程遇 IO 时主动挂起并切换至就绪任务,让 CPU 始终处理有效工作,避免资源浪费。
- 简化异步代码复杂度 :协程用 "同步风格" 写异步逻辑(如
await),避免回调地狱,代码更易读易维护。 - 高并发场景资源优势:处理大量并发任务时,协程内存占用远低于线程(单协程 KB 级 vs 线程 MB 级),单机可支撑更高并发(如爬虫、Web 服务)。
**线程与协程的关键区别:**线程是 "抢占式"(操作系统强制切换),协程是 "协作式"(主动让出);协程不替代线程,而是作为轻量级补充优化 IO 密集型场景,CPU 密集型仍需多线程 / 进程。
(2)事件循环是什么
事件循环是协程的"调度中心":在单线程内持续循环,负责管理、调度、挂起 / 恢复任务 ,让 CPU 始终处理可执行代码,避免 IO 等待浪费。它是协程并发的核心,由asyncio库实现。事件循环的核心职责:
| 核心职责 | 说明 |
|---|---|
| 管理任务队列 | 维护就绪、挂起两类任务列表,跟踪所有协程任务状态 |
| 调度执行 | 从就绪队列取出任务执行,任务遇到await主动挂起,或是执行完毕就交出控制权 |
| 处理 IO 与唤醒 | 任务执行 IO 操作时挂起;IO 完成后,将该任务重新放回就绪队列等待调度 |
| 循环直到空 | 持续循环调度任务,当所有任务都处理完毕、队列为空时,事件循环结束 |
协程 = 函数(异步函数)可挂起 / 恢复
事件循环 = 负责何时跑 / 挂 / 恢复的调度器
(3)定义中的"用户态"怎么理解
用户态的意思就是非操作系统调度。事件循环在用户态也就是Python程序内运行,切换成本远低于线程(无内核介入)。站在CPU的视角,它完全看不到协程。这是理解协程与线程、进程核心区别的关键。具体原因如下:
- CPU 只识别线程 / 进程 :CPU 的调度单位是操作系统提供的线程或进程,它只负责执行指令,完全不知道 "协程" 的存在。协程是编程语言(如 Python)在用户态通过代码逻辑实现的抽象,对操作系统和 CPU 透明。
- 协程在单个线程内运行:协程的调度由程序内部的 "事件循环" 管理,而非操作系统。无论一个线程内有多少协程,对 CPU 而言,它只是在执行这个线程的连续指令流。协程的 "挂起" 和 "恢复" 是程序内部的逻辑操作,CPU 对此毫无感知。
- 切换成本的本质区别:线程切换由操作系统触发,需 "保存 / 恢复现场",成本较高;协程切换是程序主动控制的 "软切换",仅在用户态跳转,成本极低。但从 CPU 角度看,这只是线程内部的正常指令执行,而非上下文切换。
| 视角 | 描述 |
|---|---|
| CPU 视角 | CPU 正在执行线程 A 的指令流,协程切换只是代码内的跳转,自始至终都属于线程 A,没有发生操作系统线程切换 |
| 程序 / 开发者视角 | 同一个线程 A 中运行多个协程,由事件循环调度;遇到 IO 就让出控制权,规避 IO 等待,实现高效并发 |
二、如何创建协程
asyncio 是 Python 用于编写协程代码的标准库,依托 async/await 语法实现异步编程。它是众多高性能 Python 异步框架的底层基础,常用于网络服务、Web 服务、数据库连接、分布式任务队列等场景。
使用 async 修饰定义的函数称为异步函数,定义异步函数本质就是定义协程。 调用异步函数,例如 add2(1),会得到一个协程对象。
【注】仅仅调用异步函数、生成协程对象,并不会启动执行协程 。协程对象创建完成后,状态为 pending(等待调度)。
python
# 普通函数定义
def add1(x):
print(x+1)
return x+1
# 异步函数的定义
async def add2(x):
print("in async fun add")
return x+2
三、启动多协程任务
asyncio.run(coro, *, debug=False) 用于运行顶层入口协程,例如下面代码中的 test1(),是启动异步程序的入口函数。
(1)多协程同步
python
import asyncio
async def test1():
print('hello')
# 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。
# 当前协程挂起时,事件循环可以去执行其他协程(任务)。
await asyncio.sleep(1)
print('world')
coroutine = test1()
print(coroutine)
asyncio.run(coroutine)
python
import asyncio
import time
async def eat():
for i in range(6):
print('正在吃饭...')
await asyncio.sleep(0.5)
# 做作业任务
async def work():
for i in range(6):
print('正在做作业...')
await asyncio.sleep(0.5)
async def main():
await eat()
await work()
if __name__ == '__main__':
asyncio.run(main())
而协程最核心的动作就是切换到其他任务,依靠什么实现切换?await 关键字。
(2)多协程异步
上面的例子中,虽然发生了切换,但这样的切换并不符合协程并发的预期。这两个协程任务并没有真正 "协作" 起来,本质上仍是同步串行执行。 可见,仅仅在函数内部写上 await,并不代表就能实现协程的并发效果。想要让多个协程并发运行,我们需要并发启动它们。
python
import asyncio
import time
async def eat():
for i in range(6):
print('正在吃饭...')
await asyncio.sleep(0.5)
# 做作业任务
async def work():
for i in range(6):
print('正在做作业...')
await asyncio.sleep(0.5)
async def main():
await asyncio.gather(eat(), work())
if __name__ == '__main__':
asyncio.run(main())
如果不借助 asyncio.gather,即便使用 async 定义函数,协程也会按同步方式串行执行,无法实现并发。asyncio.gather 的核心作用就是把多个协程任务交给事件循环并发调度,实现协程之间的协作。
(3)多任务异步
我们可以使用 asyncio.create_task() 封装 eat 和 work 这两个协程,调用后返回异步任务对象;再通过 await 等待任务完成。这样两个独立的协程都会被注册到同一个事件循环(Eventloop)中。代码写法看起来是顺序书写,实际却是异步并发执行。
python
import asyncio
import time
async def eat():
for i in range(6):
print('正在吃饭...')
await asyncio.sleep(0.5)
# 做作业任务
async def work():
for i in range(6):
print('正在做作业...')
await asyncio.sleep(0.5)
async def main():
# await eat()
# await work()
await asyncio.gather(eat(), work())
async def create_task():
task1 = asyncio.create_task(eat())
task2 = asyncio.create_task(work())
await task1
await task2
#await asyncio.gather(task1, task2)
if __name__ == '__main__':
asyncio.run(create_task())
四、协程的返回值和监控
(1)获取协程结束后的返回值
我们已经实现了协程并发执行,现在每个异步任务执行完成后都会返回结果。那么,该如何拿到协程的返回值呢?
python
import asyncio
import time
async def eat():
for i in range(6):
print('正在吃饭...')
await asyncio.sleep(0.5)
return '吃饭完成'
# 做作业任务
async def work():
for i in range(6):
print('正在做作业...')
await asyncio.sleep(0.5)
return '做作业完成'
async def create_task():
task1 = asyncio.create_task(eat())
task2 = asyncio.create_task(work())
# await task1
# await task2
# 得到返回值
# result = await asyncio.gather(task1, task2)
# 更加详细的返回信息
result = await asyncio.wait([task1, task2])
print(result)
if __name__ == '__main__':
asyncio.run(create_task())
asyncio.wait() 默认会等待所有任务执行完毕 (return_when=ALL_COMPLETED)。
return_when=FIRST_COMPLETED:任意一个协程完成,就立即返回return_when=FIRST_EXCEPTION:遇到第一个异常时就返回
(2)强制终止协程
task1.cancel():手动取消任务,终止协程。 如果直接手动取消 task1,抛出的异常会影响 task2 的正常执行,这和协程之间互不干扰、协作运行的特性不符。
这时可以使用 asyncio.gather() 捕获协程任务的异常。 return_exceptions=True:将协程内部的异常当作返回值返回,避免异常向外扩散、打断其他任务。
python
async def create_task():
task1 = asyncio.create_task(eat())
task2 = asyncio.create_task(work())
task1.cancel()
# await task1
# await task2
# 得到返回值
result = await asyncio.gather(task1, task2, return_exceptions=True)
# 更加详细的返回信息
# result = await asyncio.wait([task1, task2], return_when=asyncio.ALL_COMPLETED)
print(result)
if __name__ == '__main__':
asyncio.run(create_task())
(3)协程任务回调
假设协程任务执行完成后,需要立刻执行回调逻辑,例如把任务结果推送给其他接口服务,这时就可以通过 add_done_callback() 为任务添加回调函数。
python
def call_back(coroutine, current_time):
print(f'收到回调参数:{current_time}')
print(f'回调函数,任务1的返回值:{coroutine.result()}, 结束时间:{current_time}')
async def create_task():
task1 = asyncio.create_task(eat())
task2 = asyncio.create_task(work())
# 如果回调函数没有参数
# task1.add_done_callback(call_back)
# 如果回调函数有参数
task1.add_done_callback(partial(call_back, current_time=time.strftime('%H:%M:%S')))
# 得到返回值
result = await asyncio.gather(task1, task2, return_exceptions=True)
# 更加详细的返回信息
# result = await asyncio.wait([task1, task2], return_when=asyncio.ALL_COMPLETED)
print(result)
if __name__ == '__main__':
asyncio.run(create_task())
五、总结
协程任务的调度逻辑,比操作系统层面的多线程调度更加复杂。稍有不慎就会出现业务上的同步阻塞,反而达不到异步效果、弄巧成拙。这也就能解释,在很多相似场景里多线程的使用频率远高于协程:多线程无需开发者手动关注任务切换的细节。
线程与协程更适合 IO 密集型任务(例如网络请求),面对 CPU 密集型任务时性能表现较差。 CPU 密集型任务推荐使用多进程,可以规避 GIL 锁的限制,充分利用多核 CPU 提升运算效率。
高并发场景下的经典最佳实践:多进程 + 协程。既利用了多核算力,又发挥了协程轻量高效的优势,从而实现高性能。
| 方案 | 适用场景 | 优势 |
|---|---|---|
| 多线程 / 协程 | IO 密集 | 轻量,等待 IO 时切换,节约资源 |
| 多进程 | CPU 密集 | 不受 GIL 约束,利用多核 |
| 多进程 + 协程 | 高并发混合场景 | 多核算力 + 协程高并发 |