标准库、并发与 async:Rust 篇章收官

🔊

经过前四篇的旅程——所有权与借用、类型系统、错误处理、模式匹配——现在到了 Rust 篇章收官,该把零散的知识串起来了。

本篇聚焦 Rust 标准库的并发工具和 async/await 生态。这些是 Rust 与 Go 在并发领域最有趣的对标点:Go 通过 goroutine 和 channel 让并发看起来很简单,Rust 则通过严格的类型系统和所有权约束让并发变得安全可靠。

最后,我们会在文末衔接下一个主角——Zig。

std::thread:操作系统线程的直接映射

Rust 的 std::thread 提供了对操作系统线程的 1:1 映射,这与 Go 的 M:N goroutine 调度模型有本质区别。

创建线程与 join

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("子线程: {}", i);
            thread::sleep(Duration::from_millis(100));
        }
    });

    // 主线程继续执行
    for i in 1..=3 {
        println!("主线程: {}", i);
        thread::sleep(Duration::from_millis(100));
    }

    // 等待子线程结束
    handle.join().expect("子线程 panic");
}

Go 开发者注意

  • Go 的 go func() 是 goroutine(轻量级线程),Rust 的 thread::spawn 是操作系统线程(每个 goroutine 约 2KB 栈,每个 OS 线程约 2MB 栈)。
  • Go 的 main 函数返回会导致所有 goroutine 退出,Rust 的主线程结束不会自动等待子线程(需要显式 join)。
  • Go 的调度器是 M:N,Rust 是 1:1。

闭包捕获与 move

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        // move 将 v 的所有权转移给闭包
        println!("线程中: {:?}", v);
    });

    handle.join().expect("线程 panic");
    // v 的所有权已转移,这里无法访问
}

move 关键字强制将所有权转移到新线程。这与 Go 的闭包捕获不同——Go 的闭包通过指针捕获变量(共享),Rust 的闭包默认捕获引用(借用),需要 move 才能转移所有权。

消息传递:channel 的 Rust 实现

Rust 的 std::sync::mpsc 提供多生产者单消费者的 channel,与 Go 的 channel 设计理念相同——“不要通过共享内存来通信,而要通过通信来共享内存”。

mpsc::channel 基础用法

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();  // 创建 channel

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
        }
        // tx 超出作用域,channel 关闭
    });

    for received in rx {
        println!("收到: {}", received);
    }
}

Go 对比

  • Go: ch := make(chan string) / ch <- "hello" / val := <-ch
  • Rust: (tx, rx) = mpsc::channel() / tx.send(val) / rx.recv()
  • Go 的 channel 可以被多个 goroutine 读写,Rust 的 mpsc channel 只能多个发送者一个接收者(mpsc = multi-producer, single-consumer)。
  • Go 的 channel 支持缓冲和关闭检测,Rust 的 mpsc channel 也可以设置容量(mpsc::sync_channel(capacity))。

多个生产者

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    // 发送者可以被 clone
    let tx1 = tx.clone();
    let tx2 = tx.clone();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];
        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_millis(100));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];
        for val in vals {
            tx2.send(val).unwrap();
            thread::sleep(Duration::from_millis(100));
        }
    });

    // 丢弃原始的 tx,这样当两个克隆的 tx 都 drop 后,channel 会关闭
    drop(tx);

    for received in rx {
        println!("收到: {}", received);
    }
}

Go 开发者注意:Go 的 channel 不需要显式 clone,因为 channel 本身是引用类型。Rust 的 Sender 需要显式 clone 才能在多个线程中使用。

共享状态:Mutex 与 Arc

除了消息传递,Rust 也支持通过共享状态的并发模型——通过 MutexArc 实现线程安全的共享可变状态。

Mutex 基础用法

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }  // 锁在这里释放

    println!("m = {:?}", m);
}

多线程共享 Mutex(需要 Arc)

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("结果: {}", *counter.lock().unwrap());
}

Arc (Atomic Reference Counting) 提供了线程安全的引用计数,允许多个线程共享同一个所有权。

Go 对比

  • Go 没有显式的 Mutex/Arc 概念——sync.Mutex 是值类型,直接使用即可。
  • Go 的 channel 可以传递指针实现共享状态,Rust 的 Arc 是显式的共享所有权机制。
  • Go 的 mutex.Lock() / mutex.Unlock() 需要手动调用 defer 释放,Rust 的 MutexGuard 在作用域结束时自动释放(RAII)。

RwLock:读写锁

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(0));
    let mut handles = vec![];

    // 多个读者
    for _ in 0..5 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let r = data.read().unwrap();
            println!("读锁: {}", *r);
        }));
    }

    // 一个写者
    let data = Arc::clone(&data);
    handles.push(thread::spawn(move || {
        let mut w = data.write().unwrap();
        *w = 1;
        println!("写锁: 设置为 1");
    }));

    for handle in handles {
        handle.join().unwrap();
    }
}

Go 对应:Go 的 sync.RWMutex,但 Rust 的 RwLock 更严格——读锁和写锁不能同时持有。

async/await:零成本抽象的并发

Rust 1.39 引入了 async/await,提供了一种基于 Future 的零成本抽象并发模型。这与 Go 的 goroutine 本质不同——Go 的并发通过 runtime 调度器实现,Rust 的 async 通过状态机实现。

async 函数基础

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::time::Duration;
use std::thread;

async fn hello() -> String {
    println!("hello 开始");
    thread::sleep(Duration::from_millis(100));
    println!("hello 结束");
    String::from("hello")
}

async fn world() -> String {
    println!("world 开始");
    thread::sleep(Duration::from_millis(100));
    println!("world 结束");
    String::from("world")
}

#[tokio::main]
async fn main() {
    let h1 = hello();
    let h2 = world();

    let (s1, s2) = tokio::join!(h1, h2);
    println!("结果: {} {}", s1, s2);
}

关键点

  • async fn 返回一个 Future,而不是直接返回值。
  • .await 才会真正执行这个 Future(而不是调用函数时执行)。
  • tokio::join! 并发执行多个 Future。

Go 开发者注意

  • Go 的 goroutine 一启动就立即执行,Rust 的 async 函数调用时并不执行(返回 Future),需要 .await 才执行。
  • Go 的 go func() 是真正的并发,Rust 的 async fn 串行 .await 会顺序执行(需要 runtime 才能并发)。
  • Go 的 runtime 隐式调度,Rust 的 runtime 需要显式选择(tokio、async-std、smol)。

Future trait

async/await 的底层是 Future trait:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};

struct Delay {
    when: Instant,
}

impl Future for Delay {
    type Output = &'static str;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.when {
            Poll::Ready("done")
        } else {
            // 注册 waker,当时间到达时唤醒
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

async fn delay_example() {
    let delay = Delay { when: Instant::now() + Duration::from_millis(100) };
    let result = delay.await;
    println!("结果: {}", result);
}

Go 对比

  • Go 的 goroutine 直接由 runtime 管理,不需要手动实现类似 Future 的机制。
  • Rust 的 Future 更像是一个状态机,每次 poll 返回 PendingReady

tokio runtime:Rust 的并发调度器

tokio 是 Rust 最流行的 async runtime,提供了执行器、定时器、IO 等基础设施。

基础使用

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use tokio::time::{sleep, Duration};

async fn task1() {
    sleep(Duration::from_millis(100)).await;
    println!("task1 完成");
}

async fn task2() {
    sleep(Duration::from_millis(200)).await;
    println!("task2 完成");
}

#[tokio::main]
async fn main() {
    tokio::spawn(task1());
    tokio::spawn(task2());

    sleep(Duration::from_millis(300)).await;
}

Go 对比

  • Go 的 go func() 是并发执行,tokio 的 tokio::spawn 也是并发执行。
  • Go 的 main 函数返回会结束所有 goroutine,tokio 的 main async 函数返回不会结束 spawn 的任务(需要显式等待)。

select!:多路选择

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let (tx1, mut rx1) = mpsc::channel::<i32>(32);
    let (tx2, mut rx2) = mpsc::channel::<i32>(32);

    tokio::spawn(async move {
        sleep(Duration::from_millis(100)).await;
        tx1.send(1).await.unwrap();
    });

    tokio::spawn(async move {
        sleep(Duration::from_millis(200)).await;
        tx2.send(2).await.unwrap();
    });

    tokio::select! {
        Some(v) = rx1.recv() => println!("从 rx1 收到: {}", v),
        Some(v) = rx2.recv() => println!("从 rx2 收到: {}", v),
        else => println!("没有数据"),
    }
}

Go 对应:Go 的 select 语句,但 Rust 的 select! 是宏,编译期生成匹配逻辑。

对比总结:Rust vs Go 并发模型

特性GoRust
线程模型M:N goroutine(轻量级)1:1 OS 线程 + async Future
调度器内置 runtime可选 runtime(tokio、async-std)
Channel内置,双向多对多std::sync::mpsc(单向多对一)
共享状态sync.Mutex(直接使用)Mutex<Arc>(显式所有权)
错误处理panic(类似异常)Result<T, E>(类型安全)
编译期保证部分保证(race detector)完全保证(所有权+借用检查)

Go 的优势

  • 更简单的并发模型(goroutine + channel)
  • 更低的并发门槛(心智负担小)
  • 内置 runtime,无需选择
  • 开发效率高

Rust 的优势

  • 完全的并发安全(编译期保证)
  • 零成本抽象(async/await 不引入运行时开销)
  • 更细粒度的控制(可以选择不同的 runtime)
  • 更强的类型安全和错误处理

Rust 篇章小结

Rust 的并发模型体现了语言的核心哲学:安全性不妥协

通过所有权和借用检查,Rust 在编译期消除了数据竞争;通过 Future 和 async/await,Rust 提供了零成本的并发抽象;通过类型系统和 trait 机制,Rust 让并发代码既安全又高效。

但安全是有代价的——Rust 的并发代码比 Go 更冗长,心智负担更高。选择哪一种,取决于你的项目需求:追求开发效率和简单性选 Go,追求安全性和零成本抽象选 Rust。

向 Zig 的过渡

从 Go 的简单高效到 Rust 的安全零成本,我们看到了两种不同的并发哲学。Go 把并发做成语言特性,Rust 把并发做成库和类型系统。

Zig 走了第三条路:既不内置运行时,也不强推抽象

在 Zig 里,并发是显式的——你用 std.Thread 就是 OS 线程,用 std.Io.Group 就是任务组,用 Io.Evented 就是事件驱动。Zig 不会把并发封装成 go func() 那么简单,但也不会像 Rust 那样需要懂 PinUnpinWaker 这些复杂概念。

下一章,我们将深入 Zig 的世界,看看这门语言如何在"显式优先"和"零运行时"之间找到平衡。

Rust 用严格的类型系统消除并发问题,Zig 用显式的 API 让并发行为可见。不同哲学,同样的目标——编写安全高效的并发代码。

接下来,让我们迎接 Zig。