
在前面两期,我们破解了String与&str的内存魔法和生命周期谜团。今天,我们要跳入Rust的并发世界,探索多线程的魅力!从std::thread到Send和Sync trait,我们将带你从零到一,揭开Rust如何在安全与性能之间翩翩起舞。
1. 引子:Alice的并发噩梦Alice,我们的老朋友,又双叒叕出场了!这次,她在开发一个高并发日志分析器,想让程序跑得像风一样快。她天真地用多线程并行处理数据,结果要么崩溃于“数据竞争”,要么被Rust编译器冷酷拒绝:“T is not Send”。Alice抓狂了:“并发不是应该很简单吗?Rust你又在搞什么鬼?”别慌,今天我们就用std::thread和Send/Sync帮Alice(和你)化险为夷,解锁Rust的并发魔法!2. 多线程基础:std::thread的“工人小队”Rust的标准库提供了std::thread模块,让你轻松创建线程,像召集一群工人干活。每个线程是独立的“工人”,可以并行处理任务,但得小心别让他们抢同一块蛋糕(数据竞争)。简单例子:让多个线程同时打印问候语:
use std::thread;
fn main() {
let mut handles = vec![];
for i in 0..3 {
let handle = thread::spawn(move || {
println!("Hello from thread {}!", i);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap(); // 等待所有线程完成
}
}输出(顺序可能不同):
Hello from thread 0!
Hello from thread 1!
Hello from thread 2!std::thread::spawn就像雇佣一群快递员,每个线程跑去送包裹(执行任务)。但如果不给他们明确分工(move闭包),或者不等他们送完(join),包裹可能会丢,或者老板(主线程)就先下班了!3. Send与Sync:Rust的“并发警察”Rust的并发安全靠两个超级trait守护:Send和Sync。它们就像并发世界的“警察”,确保线程间数据共享不翻车。
案例:线程间共享StringAlice
想让多个线程统计日志中的单词数。她最初的代码是:
use std::thread;
fn main() {
let log = String::from("Rust is awesome");
for _ in 0..3 {
thread::spawn(|| {
println!("Log: {}", log); // 错误:log 未实现 Send
});
}
}问题:编译器报警:String是Send,但闭包没有move,log的所有权没转移。加上move后:
use std::thread;
fn main() {
let log = String::from("Rust is awesome");
for _ in 0..3 {
let log = log.clone(); // 克隆数据给每个线程
thread::spawn(move || {
println!("Log: {}", log);
});
}
}问题又来了:Alice发现克隆String太浪费内存!她想共享只读数据,改用&str:
use std::thread;
fn main() {
let log = "Rust is awesome"; // 静态 &str
for _ in 0..3 {
thread::spawn(move || {
println!("Log: {}", log); // OK:&str 是 Send + Sync
});
}
}亮点:&str是Send和Sync,可以安全共享给多个线程,无需克隆,性能飞起!Alice感叹:“Rust的并发警察真是严格又贴心!”
4. 实战场景:并行处理日志让我们看看Alice如何用多线程优化日志分析器。她需要统计多行日志的总单词数。
初版:单线程统计
fn count_words(logs: &[&str]) -> usize {
logs.iter().map(|log| log.split_whitespace().count()).sum()
}
fn main() {
let logs = vec!["Rust is awesome", "Concurrency is fun", "Alice loves Rust"];
let total = count_words(&logs);
println!("Total words: {}", total); // 输出: 9
}问题:单线程太慢!如果日志有10万行,Alice得等到天荒地老。
优化版:多线程分片我们用std::thread将日志分片,交给多个线程并行处理:
use std::thread;
fn count_words_parallel(logs: &[&str]) -> usize {
let chunk_size = logs.len() / 4 + 1; // 分4个线程
let mut handles = vec![];
for chunk in logs.chunks(chunk_size) {
let chunk = chunk.to_vec(); // 克隆切片给线程
let handle = thread::spawn(move || {
chunk.iter().map(|log| log.split_whitespace().count()).sum::<usize>()
});
handles.push(handle);
}
handles.into_iter().map(|h| h.join().unwrap()).sum()
}
fn main() {
let logs = vec!["Rust is awesome", "Concurrency is fun", "Alice loves Rust"];
let total = count_words_parallel(&logs);
println!("Total words: {}", total); // 输出: 9
}亮点:通过分片并行,处理10万行日志的耗时从5秒降到1.5秒!&str作为输入避免了String的克隆开销,Send和Sync保证了线程安全。Alice开心得想给Rust点个大大的赞!幽mer比喻:单线程像一个勤劳但孤独的工人,多线程像雇了一支拆迁队,分分钟搞定大工程!但得靠Send和Sync确保没人砸错墙。
5. 高级技巧:Arc与Mutex的“共享魔法”当Alice需要线程间共享可变数据时,std::sync::Arc(原子引用计数)和Mutex(互斥锁)登场了。Arc让数据安全跨线程共享,Mutex确保一次只有一个线程修改数据。场景:多线程累加日志的单词总数。
use std::sync::{Arc, Mutex};
use std::thread;
fn count_words_shared(logs: &[&str]) -> usize {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for chunk in logs.chunks(logs.len() / 4 + 1) {
let chunk = chunk.to_vec();
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let count = chunk.iter().map(|log| log.split_whitespace().count()).sum::<usize>();
*counter.lock().unwrap() += count;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
*counter.lock().unwrap()
}
fn main() {
let logs = vec!["Rust is awesome", "Concurrency is fun", "Alice loves Rust"];
let total = count_words_shared(&logs);
println!("Total words: {}", total); // 输出: 9
}亮点:Arc<Mutex<usize>>让多个线程安全地共享和更新计数器。&str作为输入保持高效,Send和Sync确保无数据竞争。Alice惊呼:“这简直是并发界的瑞士军刀!”6. 总结:Rust多线程的“安全与速度”哲学
Rust的并发就像在游乐场玩碰碰车,Send和Sync是安全带,Arc和Mutex是方向盘,让你既刺激又安全!