Rust 入门:编译器是你最严格的朋友 书架

第 12 章 / 共 12 章

第十二章 · 智能指针与无畏并发

当「一个主人」不够用:智能指针

第四章的铁律是「一个值一个主人」。但现实里确实存在「一份数据,几方共用」的场景——比如一棵树,多个父节点都想指向同一个子节点。Rust 的应对不是废除铁律,而是提供几种智能指针(smart pointer)——大白话是「长得像指针、但自带管理逻辑的类型」,把例外情况也装进规则的笼子。认识三个:

看出设计哲学了吗?规则一条没废,只是每条规则都提供了「编译期版」和「运行时版」,由你按场景选。默认走编译期(零开销),真有需要就用智能指针换成运行时检查(花小钱)。

无畏并发:编译器替你盯住线程

现在上终章的正题。并发——多个线程同时跑——是 bug 密度最高的编程领域,因为经典事故数据竞争(data race)太隐蔽:两个线程同时改同一块内存,结果取决于谁跑得快,不可复现、难以排查。

但在 Rust 里写多线程,你会发现一件惊人的事:数据竞争根本编译不出来。原理你已经全学过了——第五章的借用排他规矩(能看不能摸,能摸别人就不能看)在线程间照样生效,而把它搬到线程世界的,是两个特殊的标记 trait:Send(这个值能安全地过户给别的线程)和 Sync(这个值能安全地被多个线程同时看)。编译器为每个类型自动判定这两个标记;不满足的类型(比如 Rc)一旦被线程共享,当场拒编译。

use std::thread;

let v = vec![1, 2, 3];
thread::spawn(move || {
    println!("{:?}", v);   // move 闭包:v 的所有权过户进线程
});
// 此处再用 v 就编译错误——它已经属于那个线程了

要在多个线程间共享数据,用线程安全版的智能指针组合:Arc<Mutex<T>>Arc 是「原子引用计数」,Rc 的多线程版;Mutex 是互斥锁——同一时刻只放行一个线程碰数据,想改先拿锁,拿到的是一张「临时可改许可证」,用完自动还锁:

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
    let c = Arc::clone(&counter);   // 计数加一,几乎每个线程一份「共管凭证」
    handles.push(thread::spawn(move || {
        let mut n = c.lock().unwrap();  // 拿锁,许可证到手
        *n += 1;                         // 十个线程各加一次
    }));
}
for h in handles { h.join().unwrap(); }
println!("{}", counter.lock().unwrap());   // 必然是 10,分毫不差

这段代码的每一行都在执行旧规则:所有权过户给线程、共享靠计数、修改先拿锁——编译器全程核验。写漏了 lock() 直接编译不过。这就是「无畏并发」(fearless concurrency)的真实含义:不是并发变简单了,而是最危险的那类错误被提前挪到了编译期,和内存安全是同一个赌注的第二次兑现。

入门之后去哪

这本书到此为止——你已经有能力读懂大部分 Rust 代码,也有概念框架去接住剩下的东西了。给你三个方向:

最后一句话留给那个贯穿全书的问题:Rust 凭什么不用垃圾回收器也担保内存安全?现在你有了自己的答案——它把「谁拥有、谁能借、活多久、能不能跨线程」四件事全部变成编译期可核验的规则,然后由一个从不疲惫的编译器替你执行。代价是你写代码时要多回答几个问题;回报是你的程序在凌晨三点不会崩。这位最严格的朋友,值得交。

Rust 入门:编译器是你最严格的朋友 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校