Rust 的设计取向
取向 | 含义 |
|---|---|
性能不打折 | 零成本抽象:高级写法编译后和手写底层代码一样快,没有运行时开销 |
正确性优先 | 只要编译通过,一整类 bug(空指针、数据竞争、释放后使用)就不可能出现 |
显式胜于隐式 | 没有隐藏的行为,没有隐式的类型转换 |
所有权 | 每份资源——内存、文件、网络连接——同一时刻只有一个主人 |
无畏并发 | 类型系统在编译期就阻止数据竞争 |
最核心的是所有权:一个值同一时刻只属于一个变量,赋值或传参会把所有权「移走」,原来的变量就不能再用了。值的主人离开作用域时,值被自动释放,所以 Rust 既不需要垃圾回收,也不需要手动 free。
fn main() {
let data = vec![1, 2, 3];
let moved = data; // 所有权转移给 moved
// println!("{:?}", data); // 去掉注释会编译失败:data 已经被移走
println!("{:?}", moved);
}[1, 2, 3]
为什么数据与 AI 基础设施在用 Rust
越来越多的数据和 AI 基础设施用 Rust 写核心,比如 Arrow 的 Rust 实现、Lance 文件格式、Daft 多模态引擎、Polars。原因可以对应到 Rust 的具体特性上:
Rust 的特性 | 在数据与 AI 基础设施里的用处 |
|---|---|
所有权与借用检查 | 管理大块的内存缓冲区,既安全又不需要垃圾回收 |
零拷贝 | 减少图片、向量、Arrow 缓冲区在各环节之间的复制 |
Send / Sync | 在编译期保证数据在线程之间传递和共享是安全的 |
async 与 Tokio | 高并发地访问对象存储和网络 |
trait 与泛型 | 搭出可扩展的数据源、算子和索引 |
Result / Option | 显式处理脏数据和失败的样本,不会悄悄崩溃 |
SIMD 与原生性能 | 向量距离计算、解码、过滤、表达式执行 |
PyO3 | 用 Rust 写内核、对外提供 Python API,符合 AI 工程师的使用习惯 |
以 Result 为例,处理脏数据时,每一种失败都必须被显式处理:
fn parse_age(raw: &str) -> Result<u8, String> {
raw.trim()
.parse::<u8>()
.map_err(|e| format!("无法解析 {raw:?}:{e}"))
}
fn main() {
for raw in ["18", " 42 ", "abc", "300"] {
match parse_age(raw) {
Ok(age) => println!("有效:{age}"),
Err(err) => println!("跳过:{err}"),
}
}
}有效:18 有效:42 跳过:无法解析 "abc":invalid digit found in string 跳过:无法解析 "300":number too large to fit in target type
这些项目的具体用法,可以看 多模态数据闭环 系列教程。
多线程:编译器能挡住什么
并发编程大致有三种形态:
多线程:多个线程共享内存,同时执行;
异步:少量线程在大量 I/O 任务之间切换,适合「大部分时间在等」的场景;
并行:把一个大计算拆开,同时在多个核上算。
多线程最容易出错的是共享数据。下面分三类问题来看。
数据竞争:编译期就挡住了
数据竞争是指多个线程同时访问同一块内存,其中至少一个在写,而且没有同步。在 Rust 里,这样的代码根本编译不过。想在线程之间共享可变数据,必须显式地说清楚怎么共享:
做法 | 什么时候用 |
|---|---|
| 多个线程共享同一份数据,每次只有一个线程能修改 |
原子类型 + 内存序(Ordering) | 共享一个简单的计数器或标志,不想加锁 |
Channel(消息传递) | 不共享数据,而是把数据从一个线程发给另一个线程 |
Arc 让多个线程共同拥有一份数据,Mutex 保证同一时刻只有一个线程能修改它:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..8 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
*counter.lock().unwrap() += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("{}", *counter.lock().unwrap()); // 8 个线程各加 1000 次
}8000
不想共享,就用 Channel 传消息。这个例子的输出顺序每次可能不同,所以没有保存结果:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
for id in 0..3 {
let tx = tx.clone();
thread::spawn(move || {
tx.send(format!("worker {id} 完成")).unwrap();
});
}
// 丢掉最初的发送端:所有发送端都结束后,接收循环才会退出
drop(tx);
for message in rx {
println!("{message}");
}
}竞态条件:编译器挡不住
竞态条件是逻辑上的问题:每一步单独看都是安全的,但多个步骤之间被别的线程插了进来。比如取款时「先检查余额、再扣款」,如果两步分别加锁,两个线程可能都检查通过,余额就被扣成负数。规则是:需要原子完成的业务逻辑,要放在同一个临界区里。
// 错误:检查和扣款分别加锁,中间可能被别的线程插入
if *balance.lock().unwrap() >= 100 {
*balance.lock().unwrap() -= 100;
}
// 正确:在同一个临界区里完成检查和扣款
let mut balance = balance.lock().unwrap();
if *balance >= 100 {
*balance -= 100;
}锁带来的问题
问题 | 现象 | 怎么避免 |
|---|---|---|
死锁 | 两个线程各持有一把锁,又都在等对方的锁 | 所有线程按固定的顺序加锁;缩小锁的作用范围;用 |
活锁 | 线程没有阻塞,却一直在互相让步,谁也推进不了 | 让步时加入随机的等待时间 |
饥饿 | 某个线程一直抢不到锁 | 使用公平锁,或减少持锁时间 |
锁竞争 | 很多线程抢同一把锁,性能退化成串行 | 拆分锁的粒度,或改用消息传递、无锁结构 |
异步:不阻塞地等待
异步任务(Future)不是一直占着线程等待,而是「能做就做,做不了就让出线程,好了再叫我」:
.await不是阻塞等待,而是去poll一次内部的状态机,没准备好就让出线程;执行器是一个循环,不断
poll那些已经被唤醒的任务;Waker 是唤醒通知:I/O 准备好了,告诉执行器可以再来
poll一次;Pin 保证异步任务在内存中不会被移动,因为状态机里可能有指向自身的引用。
全文评论
0正在加载评论…