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

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

cover

一、学 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 是生成器语法糖,awaityield 的语义。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 异步,最大的挑战不是语法,而是思维模型——从"单线程无锁"到"多线程编译期安全"。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值