Python VS Rust: async 异步对比,有点惊喜
同样都是异步,一个快到飞起,一个慢似爬行, 今天通过对比的方式,让大家掌握这两个大难点。
- 语法相似只是巧合:二者只是都用async+await做语法糖,底层实现完全无关;
- Python异步是妥协产物:依托生成器改造、受GIL限制,只适合简单IO场景;
- Rust异步是语言原生设计:从底层设计Future模型,兼顾性能、并发、内存安全;
- 选择建议:
- 快速写爬虫、小工具、业务脚本 → 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);
}
表层相似点总结
- 函数标记:
async def/async fn - 等待语法:
await x/x.await,都只能在async函数内部使用 - 异步睡眠、并发批量等待写法逻辑高度趋同
- 异步函数不会立刻执行,调用仅创建“待执行对象”(Python协程对象 / Rust Future)
二、上层使用层面:肉眼可见的巨大区别
1. 执行机制差异
Python
async def调用后返回协程对象,不运行;必须丢进事件循环(asyncio.run)- 全局单事件循环为主,默认 单线程调度(同一时刻只有一段异步代码在跑)
- 存在GIL全局解释器锁:CPU密集任务会卡死整个异步调度,必须用多进程绕开
- 所有异步IO库必须是
asyncio兼容(aiohttp、aiomysql),普通同步requests、time.sleep会阻塞整个循环
Rust
async fn调用返回Future(惰性状态机),本身无运行时;必须搭配运行时(tokio/async-std)- Tokio默认 多线程调度器:自动分发任务到多个OS线程,CPU密集异步任务天然并行
- 无全局锁,同步阻塞函数只会阻塞当前worker线程,不会卡死全部异步任务
- 同步/异步库完全隔离,同步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 异步:基于 生成器 + 单线程事件循环
核心概念
async def本质是增强版生成器,await等价于yield- 事件循环 是一个 死循环:不断遍历
就绪的协程,恢复执行;IO等待时 把 协程挂起,让出线程 - 调度模型:协作式单线程,所有任务共用一条OS线程,任务必须主动
await让出执行权 - 数据结构:协程对象是 堆上
动态对象,GC管理;每次await保存栈状态到堆 - 瓶颈:GIL锁限制无法利用多核CPU,CPU密集任务完全不适合asyncio
执行流程简化
1. asyncio.run(main()) 创建事件循环
2. 把main协程丢进循环就绪队列
3. 循环取出任务执行,遇到await IO:
- 将协程挂起,注册IO监听
- 切换执行其他就绪协程
4. IO就绪后,重新唤醒协程继续往下跑
2. Rust 异步:基于零成本Future状态机 + 多线程调度器
核心概念
async fn编译期被翻译成状态机结构体,无运行时开销(零成本抽象)- Future 是一个
trait,只有一个poll方法:调度器调用poll推进任务,返回Pending/Ready - Tokio调度器:多线程M:N调度(M个OS线程,N个异步任务),操作系统多核并行
无堆分配:简单Future可完全存栈,不需要GC;挂起时只保存当前状态枚举,不完整保存调用栈- 无全局锁:多个线程独立调度任务,CPU密集异步任务可并行执行
执行流程简化
1. #[tokio::main] 初始化多线程worker池
2. 调用async fn生成Future(状态机结构体)
3. spawn把Future交给调度器,分配到任意空闲线程
4. 调度器反复调用Future.poll():
- 遇到.await阻塞资源,返回Poll::Pending,任务挂起
- 资源就绪后重新poll,直到返回Poll::Ready完成
底层区别 精辟总结
-
表层表象(可修改)
- 默认调度线程数:Python默认单线程,Tokio默认多线程
- 只是运行时配置,切换后不会改变底层架构
-
中层限制(语言特性锁死)
- Python有GIL,Rust无全局锁
- Python GC,Rust所有权无GC
-
底层本质(语言异步模型原生设计,无法修改)
- Python:基于解释器生成器、运行时协程对象、动态抽象
- Rust:编译期状态机Future trait、零成本静态抽象
四、皆是 epoll
Linux 下 Tokio (Rust)、asyncio (Python) 底层 IO 就绪 监听 都 依赖 epoll 多路复用 ;
但二者上层封装、事件注册/唤醒逻辑、协程挂起恢复流程有巨大差异,只是底层操作系统API共用 epoll。
一、分别拆解两者 epoll 使用流程
1. Python asyncio 底层 epoll 链路
async def协程基于生成器,执行到await io_xxx();- 把当前协程挂起,将对应的文件描述符(fd)注册到 epoll 实例;
- 事件循环阻塞调用
epoll_wait(),等待 IO 可读/可写事件; - epoll 返回就绪 fd,事件循环找到绑定的协程,恢复生成器断点继续执行。
关键点:
- asyncio 全局只有单个 epoll 实例,单线程驱动;
- 所有 IO 协程全部注册到同一个 epoll;
- 一旦同步代码阻塞,
epoll_wait无法被调用,整个IO监听卡死。
2. Rust Tokio 底层 epoll 链路
Tokio 拆分两大模块:IO Driver(epoll 线程) + 多线程Worker调度池
.awaitIO操作时,当前Future(状态机)暂停;- 将fd注册到独立专用 IO Driver 线程的 epoll;
- IO Driver 单独循环
epoll_wait,只负责监听IO就绪; - IO就绪后,IO Driver 将对应的Future投递到Worker调度队列;
- 任意空闲OS Worker线程取出Future,调用
poll()继续执行。
关键点:
- epoll 跑在独立专用线程,不和执行业务异步代码的Worker混用;
- 就算某个Worker线程被无await计算卡死,epoll监听线程不受影响,IO事件持续正常接收;
- 多Worker并行处理就绪IO任务,多核并行。
二、共性:为什么两者都要用 epoll?
Linux 高性能异步IO标准多路复用接口,作用统一:
- 不用为每个网络连接开一个线程,单线程监听成千上万fd;
- 无IO事件时阻塞,不占用CPU;有读写就绪才返回通知上层;
- 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,它就不再是普通函数,变成了生成器。
特点:
- 调用函数时,代码不会立刻执行,只会创建一个生成器对象。
- 每次调用
next(),代码运行到yield就立刻暂停。 - 暂停时会牢牢保存当前函数的局部变量、执行位置。
- 再次调用
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);
}
共性总结
- Python 协程、JS 协程:底层载体都是可暂停的生成器,依赖运行时保存执行上下文。
- 二者共享同一套架构:生成器 + Promise/Future + 单线程事件循环 Event Loop。
- Rust 异步完全脱离这条路线:没有生成器,编译为枚举状态机,没有运行时断点保存,这是本质分水岭。
六、 Python 和 JavaScript 的 异步 同宗同脉
Python 和 JavaScript 的 异步:表层语法、事件循环调度、底层依托生成器实现、协作式单线程模型高度趋同,底层细节仍有少量差异,但核心架构几乎一致。
精简一句话完整版:
JS 与 Python 的异步在 async/await 语法、单线程事件循环调度、基于可暂停生成器构建协程的核心实现逻辑上高度相似,整体底层架构近乎一模一样,仅解释器/引擎内部细节、配套API存在小幅区别。
所有评论(0)