1. 什么是协程
协程(Coroutine)是一种比线程更轻量级的并发原语。它允许函数在执行过程中挂起(suspend)并在稍后恢复(resume),从而实现非阻塞的异步编程。
与线程不同,协程的调度完全由用户态控制,不依赖操作系统的内核调度。这意味着协程的创建和切换开销远小于线程,可以在单线程内轻松创建成千上万个协程。
2. 进程、线程、协程的对比
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度方式 | 内核态调度 | 内核态调度 | 用户态调度 |
| 切换开销 | 最高(涉及进程上下文切换) | 较高(涉及内核态切换) | 极低(仅保存/恢复上下文) |
| 内存占用 | 独立地址空间,占用最大 | 每个线程默认栈空间较大 | 栈空间小,可动态增长 |
| 并发数量 | 受系统资源限制,数量最少 | 受系统资源限制 | 可轻松创建数十万级 |
| 数据竞争 | 进程间相互隔离,天然无竞争 | 需要加锁保护共享数据 | 单线程内天然避免竞争 |
| 通信方式 | 进程间通信(IPC) | 共享内存、消息传递 | 共享变量、通道(Channel) |
| 适用场景 | CPU密集型 | I/O密集型(低并发) | I/O密集型(高并发) |
3. 协程的核心概念
3.1 挂起与恢复:await关键字
协程的核心操作是挂起(suspend)和恢复(resume)。当协程遇到耗时操作(如网络请求、文件读写)时,可以主动挂起,让出 CPU 给其他协程;当操作完成后再恢复执行。
3.2 非阻塞
协程的挂起不会阻塞线程。在挂起期间,线程可以继续执行其他协程,从而充分利用 CPU 资源。
3.3 运行协程:事件循环
事件循环是管理和调度所有协程的核心机制。在Python3.7+中,我们使用asyncio.run()
启动事件循环。
3.3 结构化并发
现代协程框架(如 Python 的 asyncio)强调结构化并发:协程的生命周期与其任务(Task)绑定,主协程会等待所有子协程完成后才结束。
3.4 协程的优势
- 调度者:协程由程序自身进行调用,主动让出CPU给其他协程(通过await关键字)
- 轻量级:创建成本极低,一个线程程可以轻松创建数万个协程
- 无锁机制:同一时间只有一个协程运行,无需锁保护共享资源
- 与线程相比,协程避免了操作系统级别的上下文切换开销,只有极小的函数调用开销,在处理大量I/O操作时效率更高
4. 事件循环(Event Loop)详述
事件循环是协程并发模型的核心,它是负责调度、管理和执行所有协程的“心脏”。其工作流程如下:
4.1 注册任务
注册后,协程就被注册到事件循环中等待执行,如下3种情况都会实现注册。
asyncio.run(main_coro)(顶级注册,启动程序)
目的:启动整个异步程序。
机制:asyncio.run()负责创建EventLoop,并将传入的顶级协程main_coro隐式地包装成一个Task并运行。这个Task成为整个EventLoop的主要任务。
用途:作为程序的入口点。
asyncio.create_task(coro)(最常用,实现并发)
目的:实现并发执行。
机制:将协程对象(coroutine object)立即包装成一个Task,并将其添加到EventLoop的队列中,使其可以立即开始在后台运行,而调用者无需等待。
用途:启动多个任务,然后通过asyncio.gather()等待它们的结果。
await coro(隐式注册,实现串行)
目的:实现串行等待。
机制:当EventLoop正在运行一个协程A,协程A遇到await coro_B时,EventLoop内部会隐式地将coro_B注册为一个任务,并开始执行它。但此时EventLoop必须等待coro_B完成,才能让A协程恢复执行。
用途:确保代码按顺序执行。
4.2 启动协程
事件循环开始运行,选择一个就绪的协程开始执行。
一 般 是 在 程 序 的 同 步 部 分 ( 通 常 是if__name__==“main”:) 调 用asyncio.run(main_coroutine), 并 传 入 您 的 顶 级 协 程 对 象 。asyncio.run()负 责 实 例 化 一 个EventLoop对象,并将传入的main_coroutine包装成一个Task,并将其添加到EventLoop中。
调用EventLoop对象的run_until_complete()方法。此时,EventLoop进入一个永不停止的while True循环,选择第一批就绪任务(即main_coroutineTask)开始执行。
4.3 遇到await
协程执行到await关键字,表示它即将开始一个耗时的I/O等待。让出控制权:协程主动暂停(挂起),将I/O操作注册到事件循环的监控中,然后将控制权交还给事件循环。
4.4 切换任务
事件循环发现当前协程暂停了,它会立即选择下一个处于“就绪”状态的协程继续执行。
4.5 I/O完成通知
当I/O操作(例如3秒的网络等待)完成后,操作系统会通知事件循环:“这个I/O已经准备好了!”
4.6 恢复协程
事件循环将先前挂起的协程重新标记为“就绪”,并在合适的时机让它从上次await的地方恢复执行。
4.7 终止与清理(TheEnd)
EventLoop持续运行上述1-6步。
如果EventLoop发现它的顶级Task(main)已经完成,它立即触发退出机制。
此时EventLoop强制取消所有仍在运行的子Task,然后关闭EventLoop,程序退出。
5.协程的代码示例
5.1 Python 协程基础示例
importasyncioasyncdeffetch_data():awaitasyncio.sleep(1)# 挂起 1 秒,不阻塞线程return"数据获取完成"asyncdefmain():# 创建并启动一个协程任务task=asyncio.create_task(fetch_data())print("主协程继续执行")result=awaittask# 等待子协程完成print(result)asyncio.run(main())输出: 主协程继续执行 数据获取完成5.2 Python 并发任务示例
importasyncioasyncdefworker(name,delay):awaitasyncio.sleep(delay)print(f"{name}完成")asyncdefmain():# 并发运行多个协程awaitasyncio.gather(worker("任务A",1),worker("任务B",2),worker("任务C",3),)asyncio.run(main())5. 协程的适用场景
- 高并发 I/O 密集型任务:如 Web 服务器、爬虫、消息队列消费者。
- 异步网络请求:批量请求外部 API 时避免线程阻塞。
- 生产者-消费者模式:协程天然适合流式数据处理。
- 状态机与复杂流程控制:协程可以简化状态流转逻辑。
6. 协程的注意事项
- 协程适合 I/O 密集型任务,不适合 CPU 密集型计算(此时应使用多进程或线程池)。
- 协程内部不能有阻塞调用,否则会阻塞整个线程。
- 需要合理控制协程数量,避免无限制创建导致内存压力。
- 在调试时,协程的调用栈可能不如线程直观,需要借助专门的调试工具。
7. 总结
协程是一种高效的并发编程模型,它以极低的切换开销和内存占用,为高并发 I/O 场景提供了优雅的解决方案。掌握协程的核心概念和适用场景,能够帮助开发者写出更高效、更易维护的异步代码。