并发

异步

基本概念与原理

同步 vs 异步 vs 并发 vs 并行

这些名词看似相似,但一定要区分清楚:

概念 解释 示例
同步 (Synchronous) 任务一个接一个执行,前一个没完成下一个不能开始。 打电话时等对方说完你才能说。
异步 (Asynchronous) 发出任务后不等待结果,继续干别的事。 发短信,不用等对方回。
并发 (Concurrency) 同一时间“看起来”有多个任务在执行(任务切换快)。 一个厨师做多道菜,来回切换。
并行 (Parallelism) 真正的同时执行多个任务(多核CPU并行)。 多个厨师同时做不同的菜。

Python 异步属于 并发 的一种形式(单线程多任务),不是多核“并行”,但能极大提升 I/O 密集任务的性能。

同步阻塞的弊端

假设我们要同时下载 3 个网页:

import time
import requests

def download(url):
    print("开始下载", url)
    time.sleep(2)
    print("下载完成", url)

urls = ['a.com', 'b.com', 'c.com']

start = time.time()
for u in urls:
    download(u)
print("总耗时:", time.time() - start)

输出:

开始下载 a.com
下载完成 a.com
开始下载 b.com
下载完成 b.com
开始下载 c.com
下载完成 c.com
总耗时: 6.00s

这是同步阻塞:下载是串行的,一个任务没完成,下一个不能开始。

异步的核心思想

异步的本质是:

在等待I/O时不阻塞整个程序,而是切换去执行别的任务。

比如:

  • 网络请求、文件I/O、数据库查询都属于“等待”;
  • 这些时间 CPU 是空闲的;
  • 异步就利用这些空闲时间去干别的活。

事件循环(Event Loop)的概念

异步编程的核心机制就是 事件循环

  1. 注册任务 (coroutine)
  2. 启动事件循环 event loop
  3. 当任务等待I/O → 暂停
  4. I/O完成 → 恢复执行

也就是说:

  • 事件循环负责调度协程的执行
  • 当一个协程遇到等待(I/O),事件循环就去执行别的协程;
  • 这样单线程也能高效利用 CPU 时间。

Python 异步的演化历史

阶段 特点
Python 3.3 引入 yield from(生成器异步)
Python 3.5 正式加入 async / await 语法
Python 3.6+ asyncio 模块成熟,标准化
Python 3.7+ 默认事件循环 API 更加简洁(asyncio.run
Python 3.11+ 异步性能大幅提升(TaskGroup、ExceptionGroup)

异步的本质是协程(Coroutine)

你可以把协程理解成一种“能暂停和恢复执行”的函数。

普通函数执行到底就结束;
协程可以:

  • await 等待一个异步操作;
  • 等待完成后恢复原地继续执行

这样程序不会因为等待卡住,而是可以去处理别的任务。

同步 vs 异步

同步版本

import time

def func1():
    time.sleep(2)
    print("任务1完成")

def func2():
    time.sleep(2)
    print("任务2完成")

def main():
    func1()
    func2()

main()

耗时约 4 秒。

异步版本

import asyncio

async def func1():
    await asyncio.sleep(2)
    print("任务1完成")

async def func2():
    await asyncio.sleep(2)
    print("任务2完成")

async def main():
    await asyncio.gather(func1(), func2())

asyncio.run(main())

耗时约 2 秒

原因:await asyncio.sleep(2) 时任务1挂起,事件循环立刻执行任务2。

async / await 语法

为什么要有 async / await

在 Python 3.5 之前,异步代码是通过生成器和 yield from 实现的,语法复杂、可读性差。
于是 Python 3.5 引入了 新的关键字:asyncawait,使协程代码的结构更直观。

async 的作用:定义协程函数

在普通函数前加上 async,就变成了一个协程函数(coroutine function)

async def say_hello():
    print("Hello")
  • 调用普通函数:立即执行。
  • 调用协程函数:不会立即执行,而是返回一个协程对象(coroutine object)

举个例子:

async def test():
    print("Hello Async")

result = test()
print(result)

输出:

<coroutine object test at 0x0000021F6...>

注意:

调用协程函数时,代码不会执行,必须由事件循环(event loop)调度执行。

如何执行协程函数

有三种主要方式执行协程:

  1. asyncio.run()
    推荐的最高层封装方式(Python 3.7+):
    import asyncio
    
    async def main():
        print("Hello")
        await asyncio.sleep(1)
        print("World")
    
    asyncio.run(main())
    
    输出:
    Hello
    World
    
    asyncio.run() 会:
    1. 自动创建事件循环;
    2. 执行协程;
    3. 等待所有任务结束;
    4. 关闭事件循环。
  2. 手动获取事件循环(低层 API)
    import asyncio
    
    async def main():
        print("Hello")
        await asyncio.sleep(1)
        print("World")
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()
    

    这种方式更灵活,但一般只有在框架内部(如 FastAPI)才会使用。

  3. 在异步函数内部用 await
    async def func1():
        print("A")
        await asyncio.sleep(1)
        print("B")
    
    async def main():
        await func1()  # await 执行另一个协程
    
    asyncio.run(main())
    
    await 只能出现在 async 函数内部
    否则会报错:
    SyntaxError: 'await' outside async function 
    

await 的作用
await 的本质是:

暂停当前协程的执行,等待另一个协程完成,然后恢复执行。

比如:

import asyncio

async def task1():
    print("开始任务1")
    await asyncio.sleep(2)
    print("任务1完成")

async def task2():
    print("开始任务2")
    await asyncio.sleep(1)
    print("任务2完成")

async def main():
    await task1()
    await task2()

asyncio.run(main())

输出:

开始任务1
任务1完成
开始任务2
任务2完成

这是顺序执行,因为每次 await 都等待前一个协程结束。
多个协程并发执行(asyncio.gather

要真正并发执行多个协程,可以用 asyncio.gather()

import asyncio

async def task1():
    print("开始任务1")
    await asyncio.sleep(2)
    print("任务1完成")

async def task2():
    print("开始任务2")
    await asyncio.sleep(1)
    print("任务2完成")

async def main():
    await asyncio.gather(task1(), task2())

asyncio.run(main())

输出:

开始任务1
开始任务2
任务2完成
任务1完成

耗时约 2 秒,而不是 3 秒。

协程的返回值

协程可以像普通函数一样返回值:

import asyncio

async def add(x, y):
    await asyncio.sleep(1)
    return x + y

async def main():
    result = await add(3, 4)
    print(result)

asyncio.run(main())

输出:

7

协程嵌套调用

协程可以调用另一个协程:

async def inner():
    print("Inner start")
    await asyncio.sleep(1)
    print("Inner end")

async def outer():
    print("Outer start")
    await inner()
    print("Outer end")

asyncio.run(outer())

常见错误与陷阱

错误 原因
await 在 async 外使用 await 只能在 async def 中使用
忘记用 await 调用协程 协程不会自动执行,必须 await
time.sleep() 阻塞事件循环 异步程序中应使用 await asyncio.sleep()
在同步代码中调用协程 必须用 asyncio.run() 或事件循环执行

实战例子:异步下载任务

import asyncio

async def download(url):
    print(f"开始下载: {url}")
    await asyncio.sleep(2)
    print(f"下载完成: {url}")

async def main():
    urls = ['a.com', 'b.com', 'c.com']
    tasks = [download(u) for u in urls]
    await asyncio.gather(*tasks)

asyncio.run(main())

输出:

开始下载: a.com
开始下载: b.com
开始下载: c.com
下载完成: b.com
下载完成: a.com
下载完成: c.com

说明 3 个下载任务是并发执行的。

协程(Coroutine)的本质与生命周期

什么是协程(Coroutine)

概念定义

协程(Coroutine)是一种 比线程更轻量级的并发单元
它在一个线程中就可以实现并发执行,通过在函数之间主动让出执行权实现“伪并行”。

一句话解释:

“协程是可以在执行过程中暂停、并在之后恢复执行的函数。”

协程 vs 线程

对比项 协程 (Coroutine) 线程 (Thread)
切换控制 程序员控制(显式 await 操作系统控制(时间片)
上下文切换 在用户态完成,几乎无开销 需要进入内核态,成本高
并发模式 单线程内伪并发 多线程并行
同步机制 不需要锁(协作式) 需要锁防止竞争
使用场景 IO密集型 CPU密集型

协程的历史

协程并不是 Python 独有的概念,最早源自 1960 年代 ALGOL 语言。
Python 从:

  • Python 2.5 起支持 yield(生成器级协程雏形);
  • Python 3.5 起引入 async / await 原生语法;
  • Python 3.7+ 起正式稳定 asyncio 模块。

协程的底层本质

Python 中的协程实际上是一种特殊的 生成器(generator)

协程 = 生成器的高级形式

生成器可以暂停 (yield) 与恢复 (send),协程在此基础上增加了:

  • 状态管理(READY、RUNNING、SUSPENDED、FINISHED)
  • 自动调度(由事件循环驱动)
  • 语法糖async defawait

例如:

def gen_coroutine():
    x = yield "start"
    print("received:", x)
    yield "end"

g = gen_coroutine()
print(next(g))      # 输出 start
print(g.send(42))   # 输出 received: 42

这已经展示了协程的“暂停→恢复”特性。

协程的生命周期(Lifecycle)

协程的生命周期由 asyncio 或事件循环管理,可以分为以下几个阶段:

阶段 状态名称 说明
1 Created 定义了 async def 函数,但未执行
2 Scheduled 协程对象交给事件循环调度
3 Running 正在执行,直到遇到 await 或返回
4 Suspended 暂停等待另一个协程/IO 完成
5 Finished 执行完毕或抛出异常

示例:观察协程的创建与运行

import asyncio

async def my_coroutine():
    print("Start coroutine")
    await asyncio.sleep(1)
    print("End coroutine")
    return 42

async def main():
    coro = my_coroutine()
    print("协程创建完成:", coro)
    result = await coro
    print("协程结果:", result)

asyncio.run(main())

输出:

协程创建完成: <coroutine object my_coroutine at 0x...>
Start coroutine
End coroutine
协程结果: 42

说明:

  • my_coroutine() 创建了一个协程对象,但不会立即执行;
  • 只有当 await coro 时,事件循环才调度它运行;
  • 协程运行中遇到 await asyncio.sleep(1) 时会挂起,让出控制权;
  • 最后恢复执行并返回结果。

协程的暂停与恢复

核心关键字:

  • await:挂起当前协程,等待另一个协程完成;
  • async:定义异步函数,使其返回协程对象。

示例:

import asyncio

async def task(name, delay):
    print(f"{name} 开始")
    await asyncio.sleep(delay)
    print(f"{name} 结束")
    return f"{name} 完成"

async def main():
    res1 = await task("任务1", 2)
    res2 = await task("任务2", 1)
    print(res1, res2)

asyncio.run(main())

输出:

任务1 开始
任务1 结束
任务2 开始
任务2 结束
任务1 完成 任务2 完成

说明:await 会等待前一个任务完成,所以上面是“顺序执行”。

协程的调度机制(Event Loop 简介)

协程的运行依靠 事件循环(Event Loop),它是异步的“心脏”。

事件循环的核心职责:

  1. 管理任务队列;
  2. 调度协程运行;
  3. 在 I/O 完成时唤醒暂停的协程。

流程示意:

创建协程对象 → 事件循环调度 → 执行直到 await → 挂起 → IO完成唤醒 → 继续执行

asyncio 事件循环(Event Loop)原理

事件循环是什么?

事件循环(Event Loop) 是 Python 异步编程的“心脏”。

一句话解释:

事件循环负责管理所有协程的调度,决定哪个协程什么时候运行、什么时候暂停、什么时候恢复。

所有 async/await 异步任务,最终都由事件循环来统一管理。

类比理解

可以把事件循环想象成一个 “总调度员”

  • 它有一个“任务队列”;
  • 每当有协程准备好执行,就把它放进队列;
  • 遇到 await(例如 await asyncio.sleep())时,协程会主动让出执行权
  • 当被等待的 I/O 事件完成后,事件循环会再次调度这个协程继续执行

这是一种协作式调度(Cooperative Scheduling),
不同于多线程的“抢占式调度”。

事件循环的基本结构

import asyncio

async def main():
    print("Hello ...")
    await asyncio.sleep(1)
    print("... World!")

asyncio.run(main())

运行流程:

  1. 创建事件循环(loop)
  2. main() 协程封装为任务(Task)
  3. loop 启动,调度任务运行
  4. main() 执行到 await asyncio.sleep(1) 时挂起
  5. loop 暂时执行其他任务
  6. 1 秒后 I/O 完成,loop 唤醒 main(),继续执行
  7. main() 结束后,loop 关闭

实际内部机制(asyncio.run 的过程)

asyncio.run(main()) 内部做了三件事:

def run(main):
    loop = asyncio.new_event_loop()   # 1. 创建事件循环
    asyncio.set_event_loop(loop)
    result = loop.run_until_complete(main)  # 2. 执行主协程
    loop.close()                      # 3. 关闭事件循环
    return result

事件循环的关键方法与职责

事件循环对象(asyncio.AbstractEventLoop)有许多重要方法。

方法 作用
run_until_complete(task) 运行任务直到完成
run_forever() 一直运行循环(常用于服务器)
create_task(coro) 把协程包装成可调度的任务
call_soon(callback) 尽快执行回调函数
call_later(delay, callback) 延迟执行回调
stop() / close() 停止或关闭事件循环

示例:手动控制事件循环

import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(1)
    print("World")

# 手动管理事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

task = loop.create_task(say_hello())
loop.run_until_complete(task)
loop.close()

上面这段代码与 asyncio.run(say_hello()) 本质一样,只是我们手动控制了 loop 的生命周期。

事件循环的核心结构原理

事件循环的核心逻辑可以用伪代码描述如下:

while tasks_not_done:
    ready_tasks = get_ready_tasks()      # 获取可运行的协程
    for task in ready_tasks:
        try:
            task.step()                   # 运行到下一个 await
        except StopIteration:
            mark_task_done(task)
    handle_io_events()                    # 检查 I/O 是否完成

说明:

  • task.step() 是协程运行到下一个 await 的过程;
  • 当协程挂起时,事件循环会记录“等待的I/O”;
  • 一旦 I/O 完成,它会唤醒对应协程。

整个过程完全在单线程中完成,无锁、无竞争

事件循环与 I/O 多路复用(Selector)

Python 的事件循环实际上是基于 I/O 多路复用机制(如 epollselectkqueue)。

简单来说:

操作系统能同时监控多个文件描述符(socket、pipe等),
当任意一个 I/O 完成时,通知事件循环继续执行相关协程。

示意图:

┌────────────────────────────┐
│        Event Loop          │
├────────────────────────────┤
│ Tasks Queue                │
│ Sleeping Tasks             │
│ I/O Watcher (epoll/select) │
│ Callbacks Queue            │
└────────────────────────────┘

所以说:

  • 协程是语言层的并发机制;
  • 事件循环是运行时的调度引擎;
  • I/O 多路复用是操作系统层的底层支撑。

三者共同构成 Python 异步的核心体系。

事件循环的嵌套与多循环问题

Python 每个线程最多只能有一个事件循环。
常见错误:

RuntimeError: asyncio.run() cannot be called from a running event loop

原因是:

  • Jupyter Notebook、Qt、某些框架中已经有一个事件循环在运行;
  • 你又在其中调用了 asyncio.run()

解决办法:

  • await 直接运行;
  • 或使用 nest_asyncio(非标准库)。

事件循环的生命周期

阶段 说明
1. 创建(create) loop = asyncio.new_event_loop()
2. 绑定(set) asyncio.set_event_loop(loop)
3. 执行(run) loop.run_until_complete(task)
4. 关闭(close) loop.close()

Task 与 Future(任务与未来对象)

为什么要有 Task 和 Future?

前面了解到:

事件循环(Event Loop)负责调度协程运行。

但是——事件循环不能直接运行协程对象,它需要一种可追踪执行状态的“封装体”,能记录协程的:

  • 是否运行中?
  • 是否完成?
  • 是否抛出异常?
  • 最终结果是什么?

于是就有了:

  • Task:封装协程的执行单元;
  • Future:表示“将来某个时间会有结果的对象”。

Future 是什么?(未来对象)

  1. 概念定义

    Future 表示一个尚未完成的计算结果,将来某个时刻会被设置(set result)。

    可以把 Future 想象成一个“空盒子”:

    • 你先拿到盒子(Future 对象);
    • 稍后,计算完成时,盒子里才装入结果;
    • 你可以“等待”盒子装满(即 await Future)。
  2. 创建与使用 Future

    import asyncio
    
    async def main():
        loop = asyncio.get_running_loop()
    
        future = loop.create_future()  # 创建一个空Future
    
        # 模拟异步操作
        async def set_result_later():
            await asyncio.sleep(1)
            future.set_result("任务完成")
    
        # 安排任务执行
        loop.create_task(set_result_later())
    
        print("等待Future结果中...")
        result = await future
        print("结果:", result)
    
    asyncio.run(main())
    

    输出:

    等待Future结果中...
    结果: 任务完成
    

    说明:

    • Future 是“结果容器”;
    • await future 会挂起,直到 set_result() 被调用;
    • loop.create_task() 则负责调度异步任务。
  3. Future 的状态机

    状态 含义 设置方式
    Pending 未完成 默认状态
    Running 正在执行 loop 调度中
    Finished 正常完成 set_result(value)
    Cancelled 被取消 cancel()
    Exception 出现异常 set_exception(error)
  4. 手动操作 Future

    import asyncio
    
    async def main():
        fut = asyncio.Future()
        print("初始状态:", fut.done())
    
        fut.set_result("OK")
        print("是否完成:", fut.done())
        print("结果:", fut.result())
    
    asyncio.run(main())
    

    输出:

    初始状态: False
    是否完成: True
    结果: OK
    

Task 是什么?(任务对象)

  1. 定义

    Task 是 Future 的一个子类,用来自动执行协程并保存结果

    不需要手动设置结果,
    Task 会:

    • 启动协程;
    • 在协程结束时自动 set_result()
  2. 创建 Task

    import asyncio
    
    async def say_hello():
        await asyncio.sleep(1)
        return "Hello Async World"
    
    async def main():
        task = asyncio.create_task(say_hello())  # 创建任务
        print("任务已调度:", task)
        result = await task
        print("任务结果:", result)
    
    asyncio.run(main())
    

    输出:

    任务已调度: <Task pending name='Task-1' coro=<say_hello() running at ...>>
    任务结果: Hello Async World
    
  3. Task 与 Future 的关系

    对比项 Future Task
    作用 表示“未来结果” 封装并运行一个协程
    是否自动执行 会自动启动协程
    设置结果 手动调用 set_result() 协程完成后自动设置
    常用场景 自定义异步回调、网络通信底层 异步任务调度

    关系图:

    协程(Coroutine)
         ↓ 被封装
    任务(Task) → 继承 → Future
         ↓
    返回结果(通过 await 获取)
    
  4. 批量创建与等待任务

    import asyncio
    
    async def task(name, delay):
        await asyncio.sleep(delay)
        return f"{name} 完成"
    
    async def main():
        tasks = [
            asyncio.create_task(task("任务A", 2)),
            asyncio.create_task(task("任务B", 1))
        ]
    
        results = await asyncio.gather(*tasks)
        print("所有结果:", results)
    
    asyncio.run(main())
    

    输出:

    所有结果: ['任务A 完成', '任务B 完成']
    

    asyncio.gather() 会并发运行所有任务,并在全部完成后返回结果列表。

Task 的生命周期

阶段 状态 说明
1 Created create_task() 创建后,尚未运行
2 Scheduled 被加入事件循环队列
3 Running 协程正在执行
4 Pending 遇到 await 被挂起
5 Finished 协程执行完毕,Task 自动完成

可以用 task.done()task.cancelled()task.result() 查看状态。

例子:状态追踪

import asyncio

async def demo():
    await asyncio.sleep(1)
    return "Done"

async def main():
    t = asyncio.create_task(demo())
    print("初始状态:", t.done())
    await asyncio.sleep(0.5)
    print("中途状态:", t.done())
    await t
    print("结束状态:", t.done())
    print("结果:", t.result())

asyncio.run(main())

输出:

初始状态: False
中途状态: False
结束状态: True
结果: Done

Task 的取消与异常

import asyncio

async def never_end():
    try:
        while True:
            print("工作中...")
            await asyncio.sleep(0.5)
    except asyncio.CancelledError:
        print("任务被取消!")

async def main():
    t = asyncio.create_task(never_end())
    await asyncio.sleep(2)
    t.cancel()  # 取消任务
    await t

asyncio.run(main())

输出:

工作中...
工作中...
工作中...
任务被取消!

当任务被取消时,会在协程内部抛出 CancelledError

并发运行:asyncio.gather()、wait()、as_completed()

asyncio.gather():批量并发运行任务

  1. 概念
    asyncio.gather() 是最常用的并发执行函数。
    它的作用是并发执行多个协程或 Task,并在全部完成后返回结果列表
  2. 语法
    results = await asyncio.gather(*aws, return_exceptions=False)
    
    • aws:一组协程对象或 Task 对象。
    • return_exceptions
      • False(默认) → 如果某个任务抛异常,会立即传播。
      • True → 异常会作为结果返回,不会中断整个 gather。
  3. 示例:并发执行 3 个网络请求(模拟)
    import asyncio
    import random
    
    async def fetch_data(name):
        print(f"开始获取数据:{name}")
        await asyncio.sleep(random.uniform(1, 3))  # 模拟网络延迟
        print(f"完成:{name}")
        return f"结果:{name}"
    
    async def main():
        results = await asyncio.gather(
            fetch_data("用户信息"),
            fetch_data("订单信息"),
            fetch_data("库存信息")
        )
        print("全部完成:", results)
    
    asyncio.run(main())
    
    运行结果(顺序可能不同):
    开始获取数据:用户信息
    开始获取数据:订单信息
    开始获取数据:库存信息
    完成:订单信息
    完成:库存信息
    完成:用户信息
    全部完成: ['结果:用户信息', '结果:订单信息', '结果:库存信息']
    
    要点
    • 三个任务同时开始
    • gather() 会等待所有任务都完成,然后一次性返回结果列表。
    • 返回顺序与传入顺序一致,不是完成顺序。
  4. 注意事项
    • asyncio.gather() 内部会自动创建 Task
    • 如果要在中途取消任务,可以直接:
      task = asyncio.create_task(fetch_data("A"))
      task.cancel()
      
    • 一般来说,gather() 是最推荐的批量并发方式,适合大部分异步并发需求。

asyncio.wait():更灵活的等待控制

  1. 概念
    asyncio.wait()gather() 类似,但:
    • 它不会直接返回结果;
    • 它返回两个集合:
      • done:已完成的任务集合;
      • pending:未完成的任务集合;
    • 它可以设置超时时间等待条件
  2. 语法
    done, pending = await asyncio.wait(aws, timeout=None, return_when=asyncio.ALL_COMPLETED)
    
    参数 作用
    aws 可迭代对象(协程或任务)
    timeout 超时时间(秒)
    return_when 等待条件:ALL_COMPLETEDFIRST_COMPLETEDFIRST_EXCEPTION
  3. 示例:等待最先完成的任务
    import asyncio
    import random
    
    async def job(name, sec):
        print(f"{name} 开始,预计 {sec} 秒")
        await asyncio.sleep(sec)
        print(f"{name} 完成")
        return name
    
    async def main():
        tasks = [
            asyncio.create_task(job("任务A", 3)),
            asyncio.create_task(job("任务B", 1)),
            asyncio.create_task(job("任务C", 2))
        ]
    
        done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
        print("已完成任务:", [t.result() for t in done])
    
        for t in pending:
            t.cancel()
    
    asyncio.run(main())
    
    输出结果:
    任务A 开始,预计 3 秒
    任务B 开始,预计 1 秒
    任务C 开始,预计 2 秒
    任务B 完成
    已完成任务: ['任务B']
    
    要点
    • FIRST_COMPLETED 表示只等最快完成的任务;
    • 其他任务仍在运行,你可以决定是否 cancel()
    • 适合竞争请求超时机制等场景。

asyncio.as_completed():谁先完成谁先返回

  1. 概念
    asyncio.as_completed() 返回一个异步迭代器
    当某个协程完成时,会立刻产生一个结果(yield)。
  2. 语法
    for coro in asyncio.as_completed(aws):
        result = await coro
    
  3. 示例:实时处理完成结果
    import asyncio
    import random
    
    async def download_file(name):
        print(f"开始下载:{name}")
        await asyncio.sleep(random.uniform(1, 3))
        print(f"下载完成:{name}")
        return name
    
    async def main():
        tasks = [download_file(f"文件{i}") for i in range(1, 4)]
    
        for coro in asyncio.as_completed(tasks):
            result = await coro
            print(f"立即处理:{result}")
    
    asyncio.run(main())
    
    运行输出(可能顺序不同):
    开始下载:文件1
    开始下载:文件2
    开始下载:文件3
    下载完成:文件2
    立即处理:文件2
    下载完成:文件1
    立即处理:文件1
    下载完成:文件3
    立即处理:文件3
    
    要点
    • 不会等全部任务完成;
    • 谁先完成,谁的结果就先被处理;
    • 适合流式处理、实时响应、日志聚合等。

异步 I/O 实战:网络请求与文件操作

为什么异步 I/O 能显著提升性能?

在传统同步代码中:

def download():
    data = requests.get("https://example.com/data")
    with open("data.txt", "w") as f:
        f.write(data.text)

requests.get() 在等待服务器响应时,CPU 是闲置的
若你有 100 个这样的下载任务,就得一个一个排队完成。

而在异步模型中(用 aiohttp + aiofiles):

async def download(session, url):
    async with session.get(url) as resp:
        text = await resp.text()
        async with aiofiles.open("data.txt", "w") as f:
            await f.write(text)

当第一个请求在等待响应时,事件循环会切换到下一个请求

CPU 不再浪费,所有请求能“并发”地被调度。

这就是异步 I/O 的核心优势。

异步文件操作(aiofiles

  1. 概念简介

    Python 的 open() 是阻塞的。
    若你用它写文件,会卡住整个事件循环。

    所以异步文件操作要使用专门的库:aiofiles(标准异步文件库)。

  2. 安装

    pip install aiofiles
    
  3. 示例:

    • 异步写文件

      import asyncio
      import aiofiles
      
      async def write_file():
          async with aiofiles.open("example.txt", "w") as f:
              await f.write("Hello, Async World!\n")
              await f.write("This is written asynchronously.")
      
      asyncio.run(write_file())
      

      解析:

      语法 含义
      async with aiofiles.open() 异步上下文管理文件
      await f.write() 非阻塞写操作(不会卡住事件循环)
    • 异步读文件

      import asyncio
      import aiofiles
      
      async def read_file():
          async with aiofiles.open("example.txt", "r") as f:
              content = await f.read()
              print(content)
      
      asyncio.run(read_file())
      

      特点:

      • await f.read() 是异步操作;
      • 可同时处理多个文件 I/O。
    • 并发读写多个文件

      import asyncio
      import aiofiles
      
      async def process_file(filename, text):
          async with aiofiles.open(filename, "w") as f:
              await f.write(text)
          async with aiofiles.open(filename, "r") as f:
              content = await f.read()
              print(f"{filename} content:", content)
      
      async def main():
          tasks = [
              asyncio.create_task(process_file("file1.txt", "Alpha")),
              asyncio.create_task(process_file("file2.txt", "Beta")),
              asyncio.create_task(process_file("file3.txt", "Gamma")),
          ]
          await asyncio.gather(*tasks)
      
      asyncio.run(main())
      

      所有文件操作几乎“同时”完成(在同一线程内异步调度)。

异步网络请求(aiohttp

  1. 为什么不用 requests

    • requests 是同步库,会阻塞事件循环。
    • 异步任务需要非阻塞的网络库。

    aiohttp 是异步 HTTP 客户端和服务端库。

  2. 安装

    pip install aiohttp
    
  3. 示例:

    • 单个异步请求

      import asyncio
      import aiohttp
      
      async def fetch(url):
          async with aiohttp.ClientSession() as session:
              async with session.get(url) as resp:
                  text = await resp.text()
                  print(f"Fetched {len(text)} characters from {url}")
      
      asyncio.run(fetch("https://example.com"))
      

      解析:

      语法 说明
      ClientSession() 异步 HTTP 会话对象
      session.get(url) 异步 GET 请求
      await resp.text() 异步获取响应内容
    • 并发抓取多个网页

      import asyncio
      import aiohttp
      
      async def fetch(session, url):
          async with session.get(url) as resp:
              text = await resp.text()
              print(f"{url} done, size={len(text)}")
      
      async def main():
          urls = [
              "https://example.com",
              "https://httpbin.org/get",
              "https://python.org",
          ]
          async with aiohttp.ClientSession() as session:
              tasks = [asyncio.create_task(fetch(session, url)) for url in urls]
              await asyncio.gather(*tasks)
      
      asyncio.run(main())
      

      同时发出所有请求,不等待前一个完成。
      速度比同步 requests 快数倍。

    • 下载网页并异步保存文件

      import asyncio
      import aiohttp
      import aiofiles
      
      async def download_and_save(session, url):
          async with session.get(url) as resp:
              text = await resp.text()
              filename = url.split("//")[-1].replace("/", "_") + ".html"
              async with aiofiles.open(filename, "w") as f:
                  await f.write(text)
              print(f"Saved {filename}")
      
      async def main():
          urls = [
              "https://example.com",
              "https://httpbin.org/get",
              "https://python.org",
          ]
          async with aiohttp.ClientSession() as session:
              tasks = [download_and_save(session, url) for url in urls]
              await asyncio.gather(*tasks)
      
      asyncio.run(main())
      

异步 I/O 性能分析

模式 请求方式 理论性能
同步 requests 阻塞式 每次等待响应,CPU 空闲
多线程 requests 并发,但受 GIL 限制 有上下文切换开销
异步 aiohttp 单线程协作调度 高 I/O 并发,无锁、无切换负担

异步模型能极大地提升网络密集与文件密集任务的吞吐量。

异步上下文管理器、异步迭代器

异步上下文管理器(async with)基础

  1. 什么是上下文管理器?

    在同步代码中:

    with open("data.txt", "r") as f:
        data = f.read()
    

    它会自动执行两个步骤:

    1. 进入时调用 __enter__()
    2. 退出时调用 __exit__()(即使发生异常也会执行)。

    而在异步世界中,我们也希望能:

    • 异步地打开/关闭资源
    • 不阻塞事件循环

    于是 Python 3.5+ 引入了:

    async with —— 异步上下文管理器语法。

  2. 异步上下文管理器的语法与机制

    一个对象要能用于 async with
    必须实现两个特殊的异步方法:

    方法 作用
    __aenter__(self) 异步进入上下文时执行(对应 __enter__
    __aexit__(self, exc_type, exc_val, exc_tb) 异步退出上下文时执行(对应 __exit__
  3. 示例:

    • 简单的异步上下文管理器

      import asyncio
      
      class AsyncContext:
          async def __aenter__(self):
              print("进入上下文")
              await asyncio.sleep(1)
              return "异步资源就绪"
      
          async def __aexit__(self, exc_type, exc, tb):
              print("退出上下文")
              await asyncio.sleep(1)
              if exc:
                  print("捕获到异常:", exc)
      
      async def main():
          async with AsyncContext() as resource:
              print("使用资源:", resource)
      
      asyncio.run(main())
      

      输出:

      进入上下文
      使用资源: 异步资源就绪
      退出上下文
      

      工作机制:

      • async with 会调用 __aenter__() 获取资源;
      • 退出时自动调用 __aexit__() 释放资源;
      • 整个过程不会阻塞事件循环。
    • 在异步上下文中管理网络连接(实战)

      import aiohttp
      import asyncio
      
      async def main():
          async with aiohttp.ClientSession() as session:
              async with session.get("https://example.com") as resp:
                  text = await resp.text()
                  print(f"页面长度:{len(text)}")
      
      asyncio.run(main())
      

      aiohttp.ClientSession() 就是一个典型的异步上下文管理器。
      它内部会异步地:

      • 建立 HTTP 连接;
      • 在退出时自动关闭 session;
      • 保证没有资源泄漏。

自定义异步上下文管理器

  1. 为什么要自定义?

    在复杂的异步程序中,可能需要:

    • 打开/关闭数据库连接;
    • 申请/释放异步锁;
    • 管理异步资源(WebSocket、Session、File 等)。

    这时就可以自定义异步上下文管理器。

  2. 示例:自定义异步数据库连接管理器

    import asyncio
    
    class AsyncDB:
        async def __aenter__(self):
            print("正在连接数据库...")
            await asyncio.sleep(1)
            print("数据库连接成功")
            return self  # 返回可操作对象
    
        async def __aexit__(self, exc_type, exc_val, exc_tb):
            print("正在关闭数据库连接...")
            await asyncio.sleep(1)
            print("数据库连接已关闭")
    
        async def query(self, sql):
            print(f"执行查询:{sql}")
            await asyncio.sleep(0.5)
            return f"结果:[{sql}]"
    
    async def main():
        async with AsyncDB() as db:
            result = await db.query("SELECT * FROM users")
            print(result)
    
    asyncio.run(main())
    

    输出:

    正在连接数据库...
    数据库连接成功
    执行查询:SELECT * FROM users
    结果:[SELECT * FROM users]
    正在关闭数据库连接...
    数据库连接已关闭
    

异步迭代器(async for)基础

  1. 普通 for 和异步 for 的区别

    在同步代码中:

    for x in [1, 2, 3]:
        print(x)
    

    在异步代码中(比如异步读取流数据):

    async for line in async_source:
        print(line)
    

    此时 async_source 必须是一个 异步迭代器

  2. 异步迭代器协议

    一个对象能被 async for 使用,必须定义以下方法:

    方法 说明
    __aiter__(self) 返回异步迭代器对象(通常返回 self
    __anext__(self) 异步返回下一个元素(使用 await
  3. 示例:自定义异步迭代器

    import asyncio
    
    class AsyncCounter:
        def __init__(self, limit):
            self.limit = limit
            self.count = 0
    
        def __aiter__(self):
            return self
    
        async def __anext__(self):
            if self.count < self.limit:
                await asyncio.sleep(0.5)
                self.count += 1
                return self.count
            else:
                raise StopAsyncIteration
    
    async def main():
        async for num in AsyncCounter(5):
            print(f"计数:{num}")
    
    asyncio.run(main())
    

    输出:

    计数:1
    计数:2
    计数:3
    计数:4
    计数:5
    

    机制解析:

    • async for 每次循环都会 await __anext__()
    • 当抛出 StopAsyncIteration 时,循环结束;
    • 这样就能异步地产生序列。

异步生成器(async def + yield

  1. 什么是异步生成器?

    异步生成器是异步迭代器的语法糖,通过 async def + yield 实现。

  2. 示例:异步生成器

    import asyncio
    
    async def async_gen():
        for i in range(3):
            await asyncio.sleep(1)
            yield i
    
    async def main():
        async for value in async_gen():
            print(f"收到值:{value}")
    
    asyncio.run(main())
    

    输出:

    收到值:0
    收到值:1
    收到值:2
    

    特点:

    • 可用 async for 遍历;
    • 每次 yield 前都可异步等待;
    • 可方便地用于异步流、WebSocket、日志流等场景。

同步与异步的混合编程

为什么需要同步与异步混合?

在真实项目中,不可能所有代码都是异步的

许多 Python 库(特别是第三方库)依然是 同步阻塞 的。

例如:

import requests  # 同步网络库
import time

这些同步操作一旦运行,就会阻塞整个事件循环,导致异步性能完全失效。

示例:异步中混入同步阻塞代码

import asyncio
import requests

async def fetch_sync(url):
    print(f"开始请求: {url}")
    res = requests.get(url)   # 同步阻塞!
    print(f"完成: {url}, 状态码: {res.status_code}")

async def main():
    await asyncio.gather(
        fetch_sync("https://www.python.org"),
        fetch_sync("https://www.baidu.com")
    )

asyncio.run(main())

运行后结果:

开始请求: https://www.python.org
# 阻塞几秒……
完成: https://www.python.org, 状态码: 200
开始请求: https://www.baidu.com
完成: https://www.baidu.com, 状态码: 200

问题:两个任务不是并发执行,而是顺序执行。因为 requests.get() 是阻塞调用,会卡住事件循环。

所以:

  • 异步程序中如果调用阻塞函数,整个 async 都失去意义
  • 但完全不用同步库也不现实,比如:
    • 图像处理(Pillow)
    • 爬虫(requests)
    • 文件 I/O、数据库驱动、科学计算库等

所以需要一个中间方案:

在异步中安全地运行同步函数” —— 这就是 线程池 + 异步混合编程

在异步中运行同步任务 — loop.run_in_executor()

Python 的 asyncio 提供了一个非常强大的方法:

loop.run_in_executor(executor, func, *args)

作用:把一个同步函数(阻塞函数)放进线程池或进程池中异步运行

这让我们能在异步代码中“包裹”同步操作,从而避免阻塞事件循环。

示例:异步中调用同步 requests

import asyncio
import requests

def fetch(url):
    print(f"同步请求开始: {url}")
    res = requests.get(url)
    print(f"同步请求结束: {url}")
    return res.status_code

async def main():
    loop = asyncio.get_running_loop()
    tasks = [
        loop.run_in_executor(None, fetch, "https://www.python.org"),
        loop.run_in_executor(None, fetch, "https://www.baidu.com"),
        loop.run_in_executor(None, fetch, "https://www.bing.com"),
    ]
    results = await asyncio.gather(*tasks)
    print("所有任务完成:", results)

asyncio.run(main())

输出示例:

同步请求开始: https://www.python.org
同步请求开始: https://www.baidu.com
同步请求开始: https://www.bing.com
同步请求结束: https://www.python.org
同步请求结束: https://www.baidu.com
同步请求结束: https://www.bing.com
所有任务完成: [200, 200, 200]

说明:

  • 我们没有改动 requests,它仍然是同步的;
  • 但借助 loop.run_in_executor,这些同步任务在 独立线程 中并行执行;
  • asyncio 等待它们的结果时不会被阻塞。

使用自定义线程池与进程池

为什么要自定义 Executor?

前面用的是:

loop.run_in_executor(None, func, *args)

其中的 None 表示使用 默认线程池(ThreadPoolExecutor)

但在真实项目中,我们通常需要:

  • 控制线程/进程数量;
  • 区分 CPU 密集型任务与 I/O 密集型任务;
  • 管理多个线程池或进程池;
  • 控制生命周期(例如在程序退出前关闭)。

这时我们就需要 自己创建 Executor 对象

ThreadPoolExecutor:线程池执行器

ThreadPoolExecutor 来自 concurrent.futures 模块。

from concurrent.futures import ThreadPoolExecutor

构造方法

ThreadPoolExecutor(max_workers=None, thread_name_prefix='')
参数 说明
max_workers 最大线程数,默认与 CPU 数量有关
thread_name_prefix 线程名前缀,用于调试

示例:在异步中使用自定义线程池

import asyncio
from concurrent.futures import ThreadPoolExecutor
import time

def blocking_task(n):
    print(f"任务 {n} 开始")
    time.sleep(2)
    print(f"任务 {n} 结束")
    return n * n

async def main():
    loop = asyncio.get_running_loop()

    # 自定义线程池
    with ThreadPoolExecutor(max_workers=3, thread_name_prefix="worker") as pool:
        tasks = [
            loop.run_in_executor(pool, blocking_task, i)
            for i in range(5)
        ]
        results = await asyncio.gather(*tasks)
        print("结果:", results)

asyncio.run(main())

输出:

任务 0 开始
任务 1 开始
任务 2 开始
任务 3 开始
任务 4 开始
任务 0 结束
任务 1 结束
任务 2 结束
任务 3 结束
任务 4 结束
结果: [0, 1, 4, 9, 16]

分析:

  • 任务函数是同步阻塞的(time.sleep(2));
  • 通过线程池并行执行;
  • asyncio 事件循环不会被阻塞;
  • 我们可以灵活调整 max_workers 控制并发量。

ProcessPoolExecutor:进程池执行器

用于 CPU 密集型任务(如计算、加密、压缩、图像处理)。

为什么不用线程?因为 Python 的 GIL 限制线程无法并行执行 CPU 任务。
而多进程能绕过 GIL,充分利用多核 CPU。

示例:进程池执行器

import asyncio
from concurrent.futures import ProcessPoolExecutor
import math
import os

def cpu_bound_task(x):
    print(f"进程 {os.getpid()} 计算 {x}!")
    return sum(math.sqrt(i) for i in range(10_000_00))

async def main():
    loop = asyncio.get_running_loop()
    with ProcessPoolExecutor(max_workers=4) as pool:
        tasks = [
            loop.run_in_executor(pool, cpu_bound_task, i)
            for i in range(5)
        ]
        results = await asyncio.gather(*tasks)
        print("完成所有任务:", results)

asyncio.run(main())

输出:

进程 13104 计算 0!
进程 13105 计算 1!
进程 13106 计算 2!
进程 13107 计算 3!
进程 13104 计算 4!
完成所有任务: [31622.7..., 31622.7..., 31622.7..., 31622.7..., 31622.7...]

分析:

  • 每个任务在独立的进程中运行;
  • GIL 不再是瓶颈;
  • 适合 CPU 密集型计算;
  • 与异步事件循环完美协作。

在同步代码中运行异步任务

为什么要在同步代码中运行异步?

在真实项目中,可能遇到以下情况:

  • 你的主程序是同步的;
  • 但你想调用一个异步 API;
  • 或者你的框架(如 Flask)本身不支持 async 函数。

例如:

async def fetch_data():
    await asyncio.sleep(1)
    return "数据获取完成!"

# 不能直接这样
result = fetch_data()

会得到一个 coroutine 对象,而不是结果。

所以我们需要一种方式,让同步代码能安全地执行异步任务

方法一:asyncio.run() —— 运行整个异步程序

这是最常见、最推荐的方式。
它会创建一个事件循环,运行直到任务完成,然后关闭循环。

import asyncio

async def fetch_data():
    print("开始获取数据……")
    await asyncio.sleep(1)
    print("数据获取完成!")
    return {"status": "ok", "data": [1, 2, 3]}

def main():
    result = asyncio.run(fetch_data())
    print("同步层获得结果:", result)

main()

输出:

开始获取数据……
数据获取完成!
同步层获得结果: {'status': 'ok', 'data': [1, 2, 3]}

特点:

优点 缺点
使用简单 只能调用一次(全局事件循环只能运行一次)
自动关闭事件循环 无法与已有的事件循环嵌套运行

注意:

asyncio.run() 不能在已有事件循环中再次调用。
比如在 Jupyter Notebook、FastAPI 里,你会得到这个错误:

RuntimeError: asyncio.run() cannot be called from a running event loop

方法二:loop.run_until_complete()

适用于你自己管理事件循环的情况(较底层)。

import asyncio

async def task():
    await asyncio.sleep(1)
    return "OK"

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(task())
loop.close()

print("结果:", result)

一般不用手动这样写,除非你在自定义框架或调度器中控制 loop。

方法三:asyncio.to_thread() —— 在异步中运行同步代码(反向的反向)

这个方法出现在 Python 3.9+,是一个更优雅的方式,把同步函数丢进线程池中执行。

它其实是 run_in_executor() 的简化形式。

示例:异步函数中调用同步任务

import asyncio
import time

def blocking_io():
    print("开始执行阻塞操作")
    time.sleep(2)
    print("阻塞操作完成")
    return 42

async def main():
    print("异步任务开始")
    result = await asyncio.to_thread(blocking_io)
    print("异步任务结束,结果:", result)

asyncio.run(main())

输出:

异步任务开始
开始执行阻塞操作
阻塞操作完成
异步任务结束,结果: 42

分析:

  • to_thread() 自动在默认线程池中运行同步函数;
  • 不需要显式调用 loop.run_in_executor()
  • 写法更简洁,也不需要管理线程池对象。

综合示例:同步主程序调用异步爬虫任务

import asyncio
import requests

# 同步请求函数
def sync_fetch(url):
    print(f"[同步函数] 开始请求 {url}")
    res = requests.get(url)
    print(f"[同步函数] 完成请求 {url},状态码:{res.status_code}")
    return res.status_code

# 异步包装函数
async def async_fetch(url):
    # 使用 asyncio.to_thread 在异步中运行同步函数
    return await asyncio.to_thread(sync_fetch, url)

# 同步主程序入口
def run_async_task():
    urls = [
        "https://www.baidu.com",
        "https://www.python.org",
        "https://www.bing.com"
    ]
    results = asyncio.run(asyncio.gather(*(async_fetch(url) for url in urls)))
    print("全部任务完成:", results)

if __name__ == "__main__":
    run_async_task()

运行结果示例

[同步函数] 开始请求 https://www.baidu.com
[同步函数] 开始请求 https://www.python.org
[同步函数] 开始请求 https://www.bing.com
[同步函数] 完成请求 https://www.python.org,状态码:200
[同步函数] 完成请求 https://www.baidu.com,状态码:200
[同步函数] 完成请求 https://www.bing.com,状态码:200
全部任务完成: [200, 200, 200]

分析说明:

关键点 说明
requests.get() 是阻塞函数 所以不能直接 await
asyncio.to_thread() 将同步函数扔进线程池执行
asyncio.gather() 并发运行多个异步任务
asyncio.run() 在同步入口中启动事件循环
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐