【学习心得】Python协程

一、协程是什么

死板定义必背,但不好理解:

  1. 协程是一种可以被挂起,并且在挂起之后,还可以恢复执行的函数。
  2. 协程又称微线程,是一种用户态内的上下文切换技术。

(1)为什么需要协程

我们需要协程,核心原因是它能以极低的资源开销高效处理高并发场景,尤其擅长应对网络请求、文件读写等I/O密集型任务。弥补了传统线程模型的不足,具体优势如下:

  1. 避免线程切换的高成本:线程由操作系统调度,频繁切换需保存 / 恢复上下文,开销大;协程由程序(事件循环)主动调度,切换仅在用户态 "挂起 - 恢复",成本近乎零,单线程可以轻松管理数万协程。
  2. 最大化 CPU 利用率:传统单线程遇 IO 等待(如网络请求)会闲置 CPU;协程遇 IO 时主动挂起并切换至就绪任务,让 CPU 始终处理有效工作,避免资源浪费。
  3. 简化异步代码复杂度 :协程用 "同步风格" 写异步逻辑(如await),避免回调地狱,代码更易读易维护。
  4. 高并发场景资源优势:处理大量并发任务时,协程内存占用远低于线程(单协程 KB 级 vs 线程 MB 级),单机可支撑更高并发(如爬虫、Web 服务)。

**线程与协程的关键区别:**线程是 "抢占式"(操作系统强制切换),协程是 "协作式"(主动让出);协程不替代线程,而是作为轻量级补充优化 IO 密集型场景,CPU 密集型仍需多线程 / 进程。

(2)事件循环是什么

事件循环是协程的"调度中心":在单线程内持续循环,负责管理、调度、挂起 / 恢复任务 ,让 CPU 始终处理可执行代码,避免 IO 等待浪费。它是协程并发的核心,由asyncio库实现。事件循环的核心职责:

核心职责 说明
管理任务队列 维护就绪、挂起两类任务列表,跟踪所有协程任务状态
调度执行 从就绪队列取出任务执行,任务遇到await主动挂起,或是执行完毕就交出控制权
处理 IO 与唤醒 任务执行 IO 操作时挂起;IO 完成后,将该任务重新放回就绪队列等待调度
循环直到空 持续循环调度任务,当所有任务都处理完毕、队列为空时,事件循环结束

协程 = 函数(异步函数)可挂起 / 恢复

事件循环 = 负责何时跑 / 挂 / 恢复的调度器

(3)定义中的"用户态"怎么理解

用户态的意思就是非操作系统调度。事件循环在用户态也就是Python程序内运行,切换成本远低于线程(无内核介入)。站在CPU的视角,它完全看不到协程。这是理解协程与线程、进程核心区别的关键。具体原因如下:

  1. CPU 只识别线程 / 进程 :CPU 的调度单位是操作系统提供的线程或进程,它只负责执行指令,完全不知道 "协程" 的存在。协程是编程语言(如 Python)在用户态通过代码逻辑实现的抽象,对操作系统和 CPU 透明。
  2. 协程在单个线程内运行:协程的调度由程序内部的 "事件循环" 管理,而非操作系统。无论一个线程内有多少协程,对 CPU 而言,它只是在执行这个线程的连续指令流。协程的 "挂起" 和 "恢复" 是程序内部的逻辑操作,CPU 对此毫无感知。
  3. 切换成本的本质区别:线程切换由操作系统触发,需 "保存 / 恢复现场",成本较高;协程切换是程序主动控制的 "软切换",仅在用户态跳转,成本极低。但从 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 约束,利用多核
多进程 + 协程 高并发混合场景 多核算力 + 协程高并发
相关推荐
信誓旦旦的程序猿1 小时前
【Python量化实战宝典 #22】涨跌停、连板情绪手册:情绪指标构建(Python 可跑)
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
gCode Teacher 格码致知1 小时前
Pandas教学-21:subplots的使用
python·pandas·1024程序员节
2601_967980112 小时前
Python开发的顶级开源项目全景图:2025-2026年生态概览
python·机器学习·开源项目·数据科学·生态概览
benchmark_cc2 小时前
量化开发中,什么时候应该停止手动下载股票数据?
python·数据分析·pandas·量化交易·1024程序员节·股票数据·quantdash
泡茶喝茶写代码2 小时前
A股量化策略回测实战:从行情数据到绩效评估(第 15 篇):新高新低策略
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
全栈练习生3 小时前
Jev 决策模型
python·ai·1024程序员节
LMT流氓兔3 小时前
第五单元——第十二课:把脚本整理成项目
java·数据库·python·langchain·1024程序员节
光依旧3 小时前
day-trader-skill实测:让Claude Code当交易助理,纯Python+确定性+fail-closed
python·量化交易·ai agent·a股·风控·claude code·开源项目实测
CCblandy4 小时前
前缀缓存命中率不靠引擎靠排列:本机仿真 3 轮,易变字段放开头 98.0%→0.0%
人工智能·python·算法·自然语言处理·性能优化