同样都是异步,一个快到飞起,一个慢似爬行, 今天通过对比的方式,让大家掌握这两个大难点。

  1. 语法相似只是巧合:二者只是都用async+await做语法糖,底层实现完全无关;
  2. Python异步是妥协产物:依托生成器改造、受GIL限制,只适合简单IO场景;
  3. Rust异步是语言原生设计:从底层设计Future模型,兼顾性能、并发、内存安全;
  4. 选择建议:
    • 快速写爬虫、小工具、业务脚本 → Python async
    • 高并发网关、百万连接服务、高性能网络组件 → Rust async

一、表层语法:看着几乎一样

1. Python 异步基础模板

关键字:async def 定义异步函数,await 等待任务

import asyncio

# 异步函数
async def say_hello(name: str, delay: float):
    print(f"开始:{name}")
    # 让出控制权,等待IO,不阻塞线程
    await asyncio.sleep(delay)
    print(f"结束:{name}")
    return f"完成-{name}"

# 主入口
async def main():
    # 串行执行
    res1 = await say_hello("任务A", 1)
    res2 = await say_hello("任务B", 1)
    print(res1, res2)

    # 并发执行
    tasks = [
        say_hello("并发1", 2),
        say_hello("并发2", 2)
    ]
    results = await asyncio.gather(*tasks)
    print("并发结果:", results)

if __name__ == "__main__":
    # Python必须手动启动事件循环
    asyncio.run(main())

2. Rust 异步基础模板(tokio 运行时,工业标准)

关键字:async fn 定义异步函数,.await 等待Future

// Cargo.toml
// [dependencies]
// tokio = { version = "1.0", features = ["full"] }

#[tokio::main] // 宏自动创建异步运行时
async fn say_hello(name: &str, delay: f64) -> String {
    println!("开始:{}", name);
    // 让出控制权,非阻塞等待
    tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay)).await;
    println!("结束:{}", name);
    format!("完成-{}", name)
}

async fn main() {
    // 串行执行
    let res1 = say_hello("任务A", 1.0).await;
    let res2 = say_hello("任务B", 1.0).await;
    println!("{} {}", res1, res2);

    // 并发执行
    let task1 = say_hello("并发1", 2.0);
    let task2 = say_hello("并发2", 2.0);
    // tokio::join! 同时等待多个Future
    let (r1, r2) = tokio::join!(task1, task2);
    println!("并发结果:{} {}", r1, r2);
}

表层相似点总结

  1. 函数标记:async def / async fn
  2. 等待语法:await x / x.await,都只能在async函数内部使用
  3. 异步睡眠、并发批量等待写法逻辑高度趋同
  4. 异步函数不会立刻执行,调用仅创建“待执行对象”(Python协程对象 / Rust Future)

二、上层使用层面:肉眼可见的巨大区别

1. 执行机制差异

Python

  1. async def 调用后返回协程对象,不运行;必须丢进事件循环(asyncio.run
  2. 全局单事件循环为主,默认 单线程调度(同一时刻只有一段异步代码在跑)
  3. 存在GIL全局解释器锁:CPU密集任务会卡死整个异步调度,必须用多进程绕开
  4. 所有异步IO库必须是asyncio兼容(aiohttp、aiomysql),普通同步requeststime.sleep会阻塞整个循环

Rust

  1. async fn 调用返回Future惰性状态机),本身无运行时;必须搭配运行时(tokio/async-std)
  2. Tokio默认 多线程调度器:自动分发任务到多个OS线程,CPU密集异步任务天然并行
  3. 无全局锁,同步阻塞函数只会阻塞当前worker线程,不会卡死全部异步任务
  4. 同步/异步库完全隔离,同步sleep不会污染异步调度

2. 并发任务创建方式

Python 创建并发任务:asyncio.create_task() 主动推入循环调度

task = asyncio.create_task(say_hello("后台任务", 3))
# 先做别的事
await asyncio.sleep(1)
res = await task

Rust 创建并发任务:tokio::spawn() 生成独立任务,分配到线程池

let handle = tokio::spawn(say_hello("后台任务", 3.0));
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
let res = handle.await.unwrap();

3. 内存与生命周期

  • Python:全GC自动回收,异步函数随便传字符串、对象,不用管生命周期,但大量并发会产生内存碎片、延迟GC卡顿
  • Rust:无GC,Future严格绑定生命周期,异步函数不能随意持有引用,必须处理'static、所有权,编译期杜绝内存泄漏、野指针

4. 错误处理

Python:统一try/except,异常可跨await传播,运行时才爆错
Rust:异步返回Result<T, E>,错误编译期强制处理,不处理直接编译失败

5. 阻塞坑示例对比

Python 致命错误(同步sleep阻塞整个事件循环)

import time
async def bad():
    # 同步阻塞,所有异步任务全部卡住
    time.sleep(2)

Rust 影响有限(仅阻塞当前线程,其他线程正常跑)

async fn bad() {
    // std::thread::sleep阻塞当前worker线程,其余线程任务正常调度
    std::thread::sleep(std::time::Duration::from_secs(2));
}

三、底层核心原理(最简单通俗讲解)

1. Python 异步:基于 生成器 + 单线程事件循环

核心概念

  1. async def 本质是增强版 生成器await 等价于 yield
  2. 事件循环 是一个 死循环:不断遍历 就绪的 协程,恢复执行;IO等待时 把 协程 挂起,让出线程
  3. 调度模型:协作式单线程,所有任务共用一条OS线程,任务必须主动await让出执行权
  4. 数据结构:协程对象是 堆上 动态对象,GC管理;每次await保存栈状态到堆
  5. 瓶颈:GIL锁限制无法利用多核CPU,CPU密集任务完全不适合asyncio

执行流程简化

1. asyncio.run(main()) 创建事件循环
2. 把main协程丢进循环就绪队列
3. 循环取出任务执行,遇到await IO:
   - 将协程挂起,注册IO监听
   - 切换执行其他就绪协程
4. IO就绪后,重新唤醒协程继续往下跑

2. Rust 异步:基于零成本Future状态机 + 多线程调度器

核心概念

  1. async fn 编译期被翻译成状态机结构体,无运行时开销(零成本抽象)
  2. Future 是一个trait,只有一个poll方法:调度器调用poll推进任务,返回Pending/Ready
  3. Tokio调度器:多线程M:N调度(M个OS线程,N个异步任务),操作系统多核并行
  4. 无堆分配:简单Future可完全存栈,不需要GC;挂起时只保存当前状态枚举,不完整保存调用栈
  5. 无全局锁:多个线程独立调度任务,CPU密集异步任务可并行执行

执行流程简化

1. #[tokio::main] 初始化多线程worker池
2. 调用async fn生成Future(状态机结构体)
3. spawn把Future交给调度器,分配到任意空闲线程
4. 调度器反复调用Future.poll():
   - 遇到.await阻塞资源,返回Poll::Pending,任务挂起
   - 资源就绪后重新poll,直到返回Poll::Ready完成

底层区别 精辟总结

  1. 表层表象(可修改)

    • 默认调度线程数:Python默认单线程,Tokio默认多线程
    • 只是运行时配置,切换后不会改变底层架构
  2. 中层限制(语言特性锁死)

    • Python有GIL,Rust无全局锁
    • Python GC,Rust所有权无GC
  3. 底层本质(语言异步模型原生设计,无法修改)

    • Python:基于解释器生成器、运行时协程对象、动态抽象
    • Rust:编译期状态机Future trait、零成本静态抽象

四、皆是 epoll

Linux 下 Tokio (Rust)、asyncio (Python) 底层 IO 就绪 监听 都 依赖 epoll 多路复用
但二者上层封装、事件注册/唤醒逻辑、协程挂起恢复流程有巨大差异,只是底层操作系统API共用 epoll。

一、分别拆解两者 epoll 使用流程

1. Python asyncio 底层 epoll 链路

  1. async def 协程基于生成器,执行到 await io_xxx()
  2. 把当前协程挂起,将对应的文件描述符(fd)注册到 epoll 实例;
  3. 事件循环阻塞调用 epoll_wait(),等待 IO 可读/可写事件;
  4. epoll 返回就绪 fd,事件循环找到绑定的协程,恢复生成器断点继续执行。

关键点:

  • asyncio 全局只有单个 epoll 实例,单线程驱动;
  • 所有 IO 协程全部注册到同一个 epoll;
  • 一旦同步代码阻塞,epoll_wait 无法被调用,整个IO监听卡死。

2. Rust Tokio 底层 epoll 链路

Tokio 拆分两大模块:IO Driver(epoll 线程) + 多线程Worker调度池

  1. .await IO操作时,当前Future(状态机)暂停;
  2. 将fd注册到独立专用 IO Driver 线程的 epoll;
  3. IO Driver 单独循环 epoll_wait,只负责监听IO就绪;
  4. IO就绪后,IO Driver 将对应的Future投递到Worker调度队列;
  5. 任意空闲OS Worker线程取出Future,调用poll()继续执行。

关键点:

  • epoll 跑在独立专用线程,不和执行业务异步代码的Worker混用;
  • 就算某个Worker线程被无await计算卡死,epoll监听线程不受影响,IO事件持续正常接收;
  • 多Worker并行处理就绪IO任务,多核并行。

二、共性:为什么两者都要用 epoll?

Linux 高性能异步IO标准多路复用接口,作用统一:

  1. 不用为每个网络连接开一个线程,单线程监听成千上万fd
  2. 无IO事件时阻塞,不占用CPU;有读写就绪才返回通知上层;
  3. Python、Rust、Node.js 高性能异步IO库在Linux下默认底层都是epoll(Windows换IOCP,macOS换kqueue)。

三、关键底层区别(同样epoll,上层流程完全不同)

1. 线程模型差异

  • Python asyncio:epoll 和业务协程共用同一个主线程
    执行协程代码 ↔ epoll_wait 交替执行,一方阻塞另一方停滞。
  • Tokio:epoll IO Driver 独立线程,和业务Worker池完全隔离
    业务计算阻塞Worker,完全不干扰epoll事件监听。

2. 挂起载体不同

  • Python:IO等待时保存生成器协程堆上下文,运行时动态对象;
  • Rust:IO等待时仅保存Future状态机,栈优先分配,无运行时堆协程对象。

3. 任务分发逻辑

  • Python:epoll拿到就绪事件,立刻在当前线程恢复协程执行;串行处理所有就绪IO;
  • Rust:epoll线程仅负责收集事件,把任务丢到多线程队列,多核并发处理IO回调逻辑。

4. 阻塞耐受度

Python:同步sleep/密集计算阻塞主线程 → epoll_wait 无法调用,新IO全部失联;
Rust:Worker线程阻塞仅影响单个线程,IO Driver持续跑epoll,新IO事件正常收集、分发到其他空闲Worker。

四、极简代码验证底层依赖(补充佐证)

Python 查看asyncio底层epoll

import asyncio

async def test():
    await asyncio.sleep(1)

loop = asyncio.get_event_loop()
# Linux下返回 SelectorEventLoop,底层封装epoll
print(type(loop._selector))

Rust Tokio 底层IO Driver默认epoll

Tokio 编译时自动适配平台:

  • Linux:mio库封装epoll;
  • macOS/BSD:kqueue;
  • Windows:IOCP。
    mio是Rust底层跨平台多路复用库,Tokio IO层完全基于mio实现epoll封装。

总结

Linux环境下两者依靠同一操作系统epoll接口感知IO就绪,这是操作系统提供的公共底层能力;
epoll运行线程、任务调度分发、协程/Future挂起恢复、多线程并行能力全是两套完全不同的上层实现,不能因为共用epoll就认为异步整体逻辑一致。

五、生成器 知识补充

一、通俗讲解:到底什么是生成器?

普通函数(一次性跑完)

普通函数一旦调用,会从头到尾一口气执行完毕,中途不能暂停,执行结束栈直接销毁。

def normal_func():
    print("第一步")
    print("第二步")
    return "结束"

调用之后,整段代码一次性执行完,中途无法中断。

生成器函数:可以中途“暂停+保存现场”的函数

只要函数里出现了 yield,它就不再是普通函数,变成了生成器

特点:

  1. 调用函数时,代码不会立刻执行,只会创建一个生成器对象。
  2. 每次调用 next(),代码运行到 yield 就立刻暂停。
  3. 暂停时会牢牢保存当前函数的局部变量、执行位置。
  4. 再次调用 next(),从上次暂停的地方继续向后运行。

极简示例(Python)

def gen():
    print("执行到A")
    yield 1   # 在这里暂停,把1抛出去
    print("执行到B")
    yield 2   # 再次暂停
    print("执行完毕")

g = gen()   # 仅仅创建对象,函数代码一行都不跑

next(g)  # 运行到第一个yield,暂停 → 输出:执行到A
next(g)  # 从暂停处继续运行 → 输出:执行到B
next(g)  # 走到函数末尾,抛出结束信号

用人话总结生成器:

普通函数:一趟跑完,有去无回。
生成器:可以分段执行,随时暂停,随时续跑,能保存上下文断点。


二、生成器的 核心作用 与 意义

作用1:惰性产出数据,不用一次性把所有数据放进内存

如果要生成 1000 万条数字,列表会一次性占用大量内存。
生成器只在你需要的时候才生成下一条数据,数据现用现造,极大节省内存。

# 列表:一次性生成全部数字,占内存
nums = [i for i in range(10000000)]

# 生成器:只保存公式,不保存全部结果,几乎不占内存
def big_data():
    for i in range(10000000):
        yield i

作用2:实现“函数断点”,这就是异步协程的基石

协程的核心需求:
线程遇到网络IO、睡眠时,暂时停下当前任务,去执行别的任务;等IO完成,再回到刚才暂停的地方继续执行。

这种“暂停 + 保存现场 + 恢复执行”的能力,原生就由生成器的 yield 提供

这就是为什么 Python、JS 的异步协程都起源于生成器:

  • yield = 让出CPU,暂停函数
  • 事件循环负责监听IO事件
  • IO就绪后,再唤醒生成器,从断点继续运行

作用3:实现多任务协作(协作式多任务)

多个生成器交替执行,大家主动暂停、主动礼让CPU,不需要操作系统线程切换。
线程切换是操作系统负责(抢占式);
生成器协程是代码自己主动让出(协作式)。


三、async/await 本质 = 封装好的生成器

Python

早期协程写法:纯生成器 + yield from

@asyncio.coroutine
def old_coro():
    yield from asyncio.sleep(1)

后来推出 async def / await,只是把生成器封装起来,底层仍然是可暂停的生成器对象。

JavaScript

// 原生生成器
function* task() {
    yield Promise.resolve(1);
}
// 早期异步框架就是用生成器+Promise自动执行
// 现在 async await 只是这套逻辑的语法糖
async function task() {
    await Promise.resolve(1);
}

共性总结

  1. Python 协程、JS 协程:底层载体都是可暂停的生成器,依赖运行时保存执行上下文。
  2. 二者共享同一套架构:生成器 + Promise/Future + 单线程事件循环 Event Loop。
  3. Rust 异步完全脱离这条路线:没有生成器,编译为枚举状态机,没有运行时断点保存,这是本质分水岭。

六、 Python 和 JavaScript 的 异步 同宗同脉

Python 和 JavaScript 的 异步:表层语法、事件循环调度、底层依托生成器实现、协作式单线程模型高度趋同,底层细节仍有少量差异,但核心架构几乎一致

精简一句话完整版:
JS 与 Python 的异步在 async/await 语法、单线程事件循环调度、基于可暂停生成器构建协程的核心实现逻辑上高度相似,整体底层架构近乎一模一样,仅解释器/引擎内部细节、配套API存在小幅区别。