Python 异步编程对比 Rust:从 asyncio 到 Tokio 的思维迁移

一、学 Rust 异步之前,先搞清楚 Python 异步到底在干什么
我写 Python 异步快两年了,asyncio 用得很熟,但说实话一直没完全理解事件循环的底层机制——直到学了 Rust 的 Future 和 Tokio,回头看 Python 的 asyncio 才豁然开朗。两种语言的异步模型有本质区别:Python 的协程是单线程协作式调度,Rust 的 Future 是零成本抽象的状态机。
这篇文章不是"Python vs Rust 谁更好",而是通过对比两种异步模型的底层机制,帮助理解异步编程的本质。如果你也像我一样用 Python 异步很熟但"知其然不知其所以然",这篇可能会有帮助。
二、Python asyncio 与 Rust Tokio 的模型对比
flowchart LR
subgraph Python asyncio
A1[async def 协程] --> A2[事件循环<br/>单线程]
A2 --> A3[协程调度<br/>协作式让出]
A3 --> A4[await 挂起<br/>注册回调]
A4 --> A5[IO 就绪<br/>恢复协程]
end
subgraph Rust Tokio
B1[async fn Future] --> B2[工作窃取调度器<br/>多线程]
B2 --> B3[任务调度<br/>Poll 驱动]
B3 --> B4[Pending 挂起<br/>注册 Waker]
B4 --> B5[IO 就绪<br/>Waker 唤醒]
end
A1 -.->|生成器实现| A2
B1 -.->|状态机实现| B2
style A2 fill:#fff3e0
style B2 fill:#e3f2fd
Python asyncio 的核心是单线程事件循环 + 协程协作式调度。async def 是生成器语法糖,await 是 yield 的语义。Rust Tokio 的核心是多线程工作窃取调度器 + Future 的 Poll 模型。async fn 是状态机变换,await 是状态转换点。两者的根本区别在于:Python 的协程是运行时对象(有堆分配),Rust 的 Future 是编译期变换(零成本抽象)。
三、代码实现与分析
3.1 Python asyncio 底层机制
import asyncio
import time
from typing import Generator
# Python 协程的本质:生成器
# async def fetch(url):
# response = await aiohttp.get(url)
# return response
#
# 等价于:
class Coroutine:
"""简化版协程实现,理解 asyncio 底层"""
def __await__(self):
return (yield self)
class AsyncSleep:
"""模拟 asyncio.sleep 的底层实现"""
def __init__(self, seconds: float):
self.seconds = seconds
def __await__(self):
# yield 让出控制权给事件循环
# 事件循环在指定时间后 send() 恢复协程
yield self # 把自己交给事件循环
return None
class SimpleEventLoop:
"""简化版事件循环,理解 asyncio.run 的底层"""
def __init__(self):
self._ready = []
self._scheduled = []
def create_task(self, coro):
"""创建任务"""
task = Task(coro)
self._ready.append(task)
return task
def run_until_complete(self, coro):
"""运行直到所有任务完成"""
main_task = Task(coro)
self._ready.append(main_task)
while self._ready or self._scheduled:
# 处理就绪队列
while self._ready:
task = self._ready.pop(0)
task.step()
# 检查定时器
now = time.monotonic()
while self._scheduled and self._scheduled[0][0] <= now:
_, task = self._scheduled.pop(0)
self._ready.append(task)
if self._scheduled and not self._ready:
# 没有就绪任务,等待最近的定时器
wait_time = self._scheduled[0][0] - now
time.sleep(max(0, wait_time))
class Task:
"""简化版 Task"""
def __init__(self, coro):
self._coro = coro
def step(self):
"""驱动协程执行一步"""
try:
result = self._coro.send(None)
# result 是 await 的值(如 AsyncSleep 对象)
if isinstance(result, AsyncSleep):
# 注册定时器
wake_time = time.monotonic() + result.seconds
# 事件循环会在 wake_time 后重新 step() 这个 task
except StopIteration as e:
# 协程执行完毕
pass
# 实际 asyncio 使用
async def python_async_example():
"""Python asyncio 的典型用法"""
# 并发执行多个 IO 操作
results = await asyncio.gather(
fetch_data("api1"),
fetch_data("api2"),
fetch_data("api3"),
)
return results
async def fetch_data(name: str) -> str:
await asyncio.sleep(0.1) # 模拟 IO
return f"data from {name}"
3.2 Rust Tokio 等价实现
use tokio::time::{sleep, Duration};
use tokio::join;
/// Rust Tokio 的等价实现
async fn rust_async_example() -> Vec<String> {
// 并发执行多个 IO 操作
let (r1, r2, r3) = join!(
fetch_data("api1"),
fetch_data("api2"),
fetch_data("api3"),
);
vec![r1, r2, r3]
}
async fn fetch_data(name: &str) -> String {
sleep(Duration::from_millis(100)).await;
format!("data from {}", name)
}
/// 对比:Python 和 Rust 异步的关键差异
///
/// 1. 协程 vs Future
/// Python: async def 返回 coroutine 对象(堆分配)
/// Rust: async fn 返回 Future trait 对象(状态机,栈友好)
///
/// 2. 调度模型
/// Python: 单线程事件循环,协程协作式调度
/// Rust: 多线程工作窃取调度器,任务可跨线程迁移
///
/// 3. 错误处理
/// Python: try/except,异常可以穿透 await
/// Rust: Result<T, E>,错误必须显式处理
///
/// 4. 取消语义
/// Python: asyncio.Task.cancel() 抛 CancelledError
/// Rust: drop(Future) 直接清理,无异常
3.3 思维迁移:Python 开发者学 Rust 异步的常见坑
use tokio::sync::Mutex as TokioMutex;
use std::sync::Mutex as StdMutex;
use tokio::task;
/// 坑1:以为 async fn 会自动在后台执行
async fn pitfall_auto_run() {
// Python: asyncio.create_task() 会立即调度
// Rust: async fn 只是定义,不会执行
let future = fetch_data("api"); // 不会执行!
future.await; // 这里才执行
// 要并发执行,必须用 tokio::spawn
let handle = tokio::spawn(fetch_data("api")); // 提交到调度器
handle.await.unwrap(); // 等待结果
}
/// 坑2:在异步代码中使用 std::sync::Mutex
async fn pitfall_std_mutex() {
let mutex = StdMutex::new(0);
// Python: asyncio 是单线程,不需要锁
// Rust: Tokio 是多线程,但 std::sync::Mutex 持有锁跨 await 会死锁
let mut guard = mutex.lock().unwrap();
*guard += 1;
// some_async_op().await; // ❌ 持有 std::sync::Mutex 跨 await!
drop(guard); // ✅ 在 await 前释放
// 或者用 tokio::sync::Mutex
let async_mutex = TokioMutex::new(0);
let mut guard = async_mutex.lock().await;
*guard += 1;
// some_async_op().await; // ✅ tokio Mutex 可以跨 await
}
/// 坑3:以为 .await 会创建新线程
async fn pitfall_new_thread() {
// Python: await 会让出控制权给事件循环
// Rust: .await 只是状态机的一个状态转换点,不会创建线程
// 当前任务在 .await 处挂起,调度器执行其他任务
// 当 IO 就绪时,Waker 唤醒当前任务继续执行
let result = fetch_data("api").await;
// 这里仍在同一个工作线程上(除非被工作窃取迁移)
}
/// 坑4:忽略 Send 约束
async fn pitfall_send() {
// Python: 所有对象都可以跨线程传递(GIL 保护)
// Rust: tokio::spawn 要求 Future 是 Send
// 如果 Future 中包含 !Send 的类型(如 Rc),编译报错
use std::rc::Rc;
let data = Rc::new(42); // Rc 不是 Send
// ❌ 编译错误:Rc<i32> cannot be sent between threads safely
// tokio::spawn(async move {
// println!("{}", *data);
// });
// ✅ 用 Arc 替代 Rc
use std::sync::Arc;
let data = Arc::new(42);
tokio::spawn(async move {
println!("{}", *data);
});
}
四、两种异步模型的权衡
Python asyncio 的优势:简单直观,单线程无数据竞争,生态成熟(aiohttp、asyncpg 等),适合 IO 密集型服务。劣势:单线程 CPU 密集任务会阻塞事件循环(需用 run_in_executor),GIL 限制真正的并行。
Rust Tokio 的优势:零成本抽象,多线程真正并行,编译期保证线程安全,适合高性能场景。劣势:学习曲线陡峭(Send/Sync、Pin、生命周期),生态不如 Python 成熟,调试异步代码更困难。
迁移建议:从 Python asyncio 迁移到 Rust Tokio 时,最大的思维转变是"从单线程协作式到多线程抢占式"。Python 中不需要考虑的线程安全问题(锁、Send/Sync)在 Rust 中是编译期强制检查的。建议先理解 Rust 的所有权和生命周期,再学 Tokio——异步是建立在所有权系统之上的。
五、总结
Python asyncio 和 Rust Tokio 的异步模型有本质区别:Python 是单线程协作式调度,Rust 是多线程 Poll 驱动。本文通过对比两者的底层机制,帮助理解异步编程的本质。关键差异为:Python 协程是运行时对象,Rust Future 是编译期状态机;Python 单线程无数据竞争,Rust 多线程需要 Send/Sync 保证;Python 用异常处理错误,Rust 用 Result 显式处理。从 Python 迁移到 Rust 异步,最大的挑战不是语法,而是思维模型——从"单线程无锁"到"多线程编译期安全"。
157

被折叠的 条评论
为什么被折叠?



