Python3 asyncio 协程

多线程之外,asyncio 提供另一种并发方案——协程:在单线程内由事件循环调度多个任务,遇到 IO 时主动让出控制权,可轻松并发成百上千个连接,而代码写起来仍像同步那样顺序清晰。

async def 与 await

用 async def 定义的函数是协程函数:调用它并不立即执行,而是返回一个协程对象,需交给事件循环运行。协程内部用 await 等待另一个协程或可等待对象:

import asyncio

async def hello():           # 协程函数
    print("开始")
    await asyncio.sleep(1)   # 模拟耗时,同时把控制权让出去
    print("结束")

asyncio.run(hello())         # 事件循环入口:创建并运行
# 输出:开始 → 结束

asyncio.run:启动事件循环

asyncio.run(main()) 负责创建事件循环、运行 main 协程并在结束后关闭循环,是 Python 3.7+ 的推荐入口,脚本里只需调用一次。

依次 await 是串行

在 main 里逐个 await,任务仍是一个接一个执行,总耗时是各项之和:

import asyncio

async def task(name, sec):
    await asyncio.sleep(sec)
    print(name, "完成")

async def main():
    await task("A", 1)       # 串行:先等 A 完成
    await task("B", 1)       # 再等 B 完成,共约 2 秒

asyncio.run(main())

asyncio.gather:并发执行

把多个协程交给 asyncio.gather 即可并发调度,总耗时约等于最慢的一项,返回值按传入顺序排列:

import asyncio
import time

async def task(name, sec):
    await asyncio.sleep(sec)
    print(name, "完成")
    return name

async def main():
    t0 = time.perf_counter()
    results = await asyncio.gather(task("A", 1), task("B", 2), task("C", 1))
    print(results)                                 # 输出:['A', 'B', 'C']
    print(f"总耗时约 {time.perf_counter() - t0:.1f} 秒")  # 输出:总耗时约 2.0 秒

asyncio.run(main())

与线程的对比

  • 线程由操作系统调度,数量大时切换开销高且受 GIL 限制;协程由事件循环在单线程内调度,切换成本极低,可并发成千上万个 IO 任务;
  • 协程代码更接近同步写法,没有加锁的烦恼;但计算密集任务两者都不理想,应使用多进程。

简单示例:并发请求

asyncio 通常配合 aiohttp 等异步库发起并发网络请求:

# 需安装依赖并配置环境后运行:pip install aiohttp
import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        return resp.status

async def main():
    async with aiohttp.ClientSession() as session:
        urls = ["https://www.python.org", "https://www.baidu.com"]
        codes = await asyncio.gather(*(fetch(session, u) for u in urls))
        print(codes)   # 输出:[200, 200](视网络环境而定)

asyncio.run(main())

小结:async def/await 声明协程,asyncio.run 启动事件循环,asyncio.gather 并发执行——单线程即可支撑海量 IO 连接,配合 aiohttp 能并发请求多个网络接口。

笔记加载中…