异步 Rust —— 争议不断的主题

科技   2024-11-25 11:21   广东  

简介

让我们通过一个简单的例子来解析异步 Rust 的内部机制:

async fn read_file() -> String {
   let contents = tokio::fs::read_to_string("hello.txt").await?;
   contents.to_uppercase()
}

简单吗?还是不简单?

关键复杂性

状态机生成

编译器将以下代码:

async fn read_file() -> String { ... }

转换为类似这样的状态机:

enum ReadFileStateMachine {
   Start,
   WaitingForRead { future: ReadToStringFuture },
   Processing { contents: String },
   Done
}

内存管理

每个状态需要存储未来状态的数据。编译器内部生成的代码类似于:

enum ReadFileStateMachine {
   // 初始状态 - 无数据存储
   State0,
   
   // 等待文件读取 - 存储未来
   State1 {
       future: tokio::fs::ReadToStringFuture,
   },
   
   // 读取后,转换为大写之前 - 存储内容
   State2 {
       contents: String,
   },
   
   // 完成状态
   Done,
}

执行期间的内存布局

┌──────────────────────────────┐
│ Stack                        │
├──────────────────────────────┤
│ ReadFileStateMachine         │
│  ├──────────────────────────┤
│  │ State1                    │
│  │  └── ReadToStringFuture   │
│  │       (must be pinned)    │
│  ├──────────────────────────┤
│  │ State2                    │
│  │  └── String contents      │
│  │       (owned data)        │
└──────────────────────────────┘

┌──────────────────────────────┐
│ Heap                         │
├──────────────────────────────┤
│ File contents buffer         │
│ (managed by tokio runtime)   │
└──────────────────────────────┘

每个await 点触发状态转换:

  • State0 ->State1(开始读取文件)
  • State1 ->State2(文件读取完成)
  • State2 ->Done(转换为大写完成)

引用必须在await 点之间保持有效

未来必须被固定,因为它可能包含自引用:

pin_mut!(future);  // 内部自动固定

引用必须在await 点之间保持有效:

let contents = tokio::fs::read_to_string("hello.txt").await?;
// `contents` 现在是拥有的 String,可以在 await 之后安全使用

固定要求防止移动自引用数据

状态机本身必须被固定,因为:

  • 它包含可能是自引用的未来
  • 它需要一个稳定的内存位置供运行时轮询
Pin<Box<dyn Future<Output = String>>>

栈管理

  • 栈帧 1:异步运行时
    • 栈帧 3:read_to_string 未来
    • 栈帧 2:read_file 状态机

生命周期与借用

借用的数据必须在整个异步操作期间有效。借用检查器必须跟踪await 点之间的引用。自引用结构需要特殊处理。

异步与同步

通过以上基本理解,可以看出异步 Rust 的复杂性。我们可以通过对比同步版本来更好地理解它。

同步版本

use std::fs;
use std::error::Error;

fn read_file() -> Result<String, std::io::Error> {
   let contents = fs::read_to_string("hello.txt")?;
   Ok(contents.to_uppercase())
}

异步版本

use tokio::fs;
use std::error::Error;

async fn read_file() -> Result<String, std::io::Error> {
   let contents = fs::read_to_string("hello.txt").await?;
   Ok(contents.to_uppercase())
}

编译器生成的简化版本:

enum ReadFileStateMachine {
   Initial,
   ReadingFile {
       future: fs::ReadToStringFuture,
   },
   Processing {
       contents: String,
   },
   Done
}

异步版本:

┌──────────────────────────────┐
│ Runtime Executor             │
├──────────────────────────────┤
│  ├── State Machine           │
│  ├── Futures                 │
│  └── Task Queue              │
└──────────────────────────────┘

同步版本:

┌──────────────────────────────┐
│ Direct Execution             │
├──────────────────────────────┤
│  └── Call Stack              │
└──────────────────────────────┘

性能影响

异步开销

struct AsyncOverhead {
   state_machine: usize,     // ~24 bytes
   waker: usize,             // ~8 bytes
   future_metadata: usize,   // ~16 bytes
}

同步开销

struct SyncOverhead {
   stack_frame: usize,      // ~8 bytes
}

执行模型

异步:非阻塞,需要运行时

运行时调度器

┌──────────────────────────────┐
│ Tokio Runtime                │
├──────────────────────────────┤
│  ├── Task Queue              │ ── 多个任务可以在等待 I/O 时进展
│  ├── Event Loop              │    (运行时处理调度)
│  └── Thread Pool             │
└──────────────────────────────┘

异步代码运行时:

  • 运行时维护任务队列
  • 当任务遇到.await 时,运行时暂停它
  • 运行时切换到其他准备好的任务
  • 当 I/O 完成时,运行时重新调度任务

同步:阻塞,直接执行

调用栈(直接操作系统线程管理)

┌──────────────────────────────┐
│ main()                       │
├──────────────────────────────┤
│   └── read_file()            │ ── 操作系统线程阻塞直到 I/O 完成
│        └── read()            │    (操作系统处理调度)
└──────────────────────────────┘

同步代码运行时:

  • 线程顺序执行指令
  • 当需要 I/O 时,操作系统处理线程调度
  • 线程休眠直到 I/O 完成
  • 操作系统唤醒线程继续执行

同步代码依赖于操作系统线程调度,而异步代码需要运行时来管理任务调度和 I/O 通知。运行时增加了复杂性,但允许在更少的线程上并发执行。

错误处理

异步错误流

┌──────────────────────────────┐    ┌──────────────────────────────┐     ┌──────────────────────────────┐
│ State 1                      │    │ State 2                      │     │ State 3                      │
│ (File Open)                  │───▶│ (Reading)                    │───▶│ (Processing)                 │
└──────────────────────────────┘    └──────────────────────────────┘     └──────────────────────────────┘

错误必须在状态转换中保持。

同步错误流

┌──────────────────────────────┐
│ Call Stack                   │
├──────────────────────────────┤
│  ├── File Open               │
│  ├── Read                    │───▶ 直接错误传播
│  └── Process                 │     向上调用栈
└──────────────────────────────┘

异步:跨状态转换的错误

use tokio::fs;
use std::error::Error;

asyncfn read_file() -> Result<StringBox<dyn Error>> {
   // 错误状态 1:文件打开
   let contents = fs::read_to_string("hello.txt")
       .await
       .map_err(|e| {
           // 错误跨越状态机边界
           // 必须在 await 点之间保持
           Box::new(e) asBox<dyn Error>
       })?;

   // 错误状态 2:处理
   contents
       .to_uppercase()
       .try_into()
       .map_err(|e| Box::new(e) asBox<dyn Error>)
}

同步:直接错误传播

use std::fs;
use std::error::Error;

fn read_file() -> Result<StringBox<dyn Error>> {
   // 在同一栈上直接错误传播
   let contents = fs::read_to_string("hello.txt")?;
   
   // 单个栈帧中的错误处理
   contents
       .to_uppercase()
       .try_into()
       .map_err(|e| Box::new(e))
}

调试

异步栈追踪

#0 ReadFileStateMachine::poll
#1 tokio::runtime::task::poll
#2 tokio::runtime::scheduler::execute

同步栈追踪

#0 read_file
#1 main

内存模型

异步内存布局

┌──────────────────────────────┐
│ Task Header                  │
├──────────────────────────────┤
│ Future State                 │
└──────────────────────────────┘
┌──────────────────────────────┐
│ File Buffer                  │
└──────────────────────────────┘

同步内存布局

┌──────────────────────────────┐
│ Stack Frame                  │
└──────────────────────────────┘
┌──────────────────────────────┐
│ File Buffer                  │
└──────────────────────────────┘

通过以上深入分析,可以很清楚地看出为什么异步 Rust 引入了复杂性,但在运行时开销和更复杂的调试成本下,能够实现更好的资源利用。

点击关注并扫码添加进交流群

 免费领取「Rust 语言」学习资料  

Rust编程笔记
与你一起在Rust的世界里探索、学习、成长!
 最新文章