经过前四篇的旅程——所有权与借用、类型系统、错误处理、模式匹配——现在到了 Rust 篇章收官,该把零散的知识串起来了。
本篇聚焦 Rust 标准库的并发工具和 async/await 生态。这些是 Rust 与 Go 在并发领域最有趣的对标点:Go 通过 goroutine 和 channel 让并发看起来很简单,Rust 则通过严格的类型系统和所有权约束让并发变得安全可靠。
最后,我们会在文末衔接下一个主角——Zig。
std::thread:操作系统线程的直接映射
Rust 的 std::thread 提供了对操作系统线程的 1:1 映射,这与 Go 的 M:N goroutine 调度模型有本质区别。
创建线程与 join
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
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 基础用法
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))。
多个生产者
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 也支持通过共享状态的并发模型——通过 Mutex 和 Arc 实现线程安全的共享可变状态。
Mutex 基础用法
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)
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:读写锁
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 函数基础
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:
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 返回 Pending 或 Ready。
tokio runtime:Rust 的并发调度器
tokio 是 Rust 最流行的 async runtime,提供了执行器、定时器、IO 等基础设施。
基础使用
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!:多路选择
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 并发模型
| 特性 | Go | Rust |
|---|
| 线程模型 | 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 那样需要懂 Pin、Unpin、Waker 这些复杂概念。
下一章,我们将深入 Zig 的世界,看看这门语言如何在"显式优先"和"零运行时"之间找到平衡。
Rust 用严格的类型系统消除并发问题,Zig 用显式的 API 让并发行为可见。不同哲学,同样的目标——编写安全高效的并发代码。
接下来,让我们迎接 Zig。