多线程
官方文档“无畏并发”的标题翻译听起来像“无谓”...所以莫名感觉有点狂🤨
生(spawn)一个线程
rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
// 无论线程是否执行完毕,一旦主线程结束,多线程任务也会中断
// 此时可以使用join阻塞主线程,等待 handle 线程结束
handle.join().unwrap();
}消息传递
“不要通过共享内存来通信;相反,通过通信来共享内存。”
通道是一个通用的编程概念,有发送端和接收端两部分,通过它数据可以从一个线程发送到另一个线程。
创建通道
rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel(); // 创建一个通道,并将两个端点分别赋值给 tx(发送)和 rx(接收)
thread::spawn(move || { // move将tx移动到闭包中
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}克隆并使用发送端
多生产者,单消费者:Rust 标准库实现通道的方式意味着一个通道可以有多个发送端产生值,但只有一个接收端消费这些值
rust
// --snip--
// 创建消息通道:返回一个发送端(tx)和一个接收端(rx)
// mpsc = Multiple Producer, Single Consumer (多生产者,单消费者)
let (tx, rx) = mpsc::channel();
// 克隆发送端(tx),创建第二个发送端(tx1)
// 现在有两个发送端:tx 和 tx1(多个生产者)
let tx1 = tx.clone();
// 启动第一个生产者线程
thread::spawn(move || {
// 该线程拥有 tx1 的所有权(move 转移所有权)
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
// 循环发送消息
for val in vals {
// 通过 tx1 发送字符串消息
tx1.send(val).unwrap();
// 每次发送后暂停1秒
thread::sleep(Duration::from_secs(1));
}
}); // 线程结束,tx1 自动释放
// 启动第二个生产者线程
thread::spawn(move || {
// 该线程拥有原始 tx 的所有权(move 转移所有权)
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
// 通过 tx 发送消息
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
}); // 线程结束,tx 自动释放
// 在主线程中接收消息(单消费者)
// rx 是通道的接收端,只能有一个接收端
for received in rx {
// 从通道接收消息(自动阻塞等待)
println!("Got: {received}");
} // 当所有发送端关闭后,循环自动结束
// --snip--共享状态(共享环境)
这里终于轮到 Mutex<T> 和 Arc<T> 登场了!
Mutex<T>:一个提供了内部可变性的互斥锁(容器)。
- 像银行金库的门:一次只允许一人进入
- 通过
.lock()获取访问权(钥匙) - 离开作用域时自动释放锁(还钥匙)
Arc<T>:原子引用计数(使用了标准库atomic实现的引用计数指针),一个线程安全的智能指针。
- 让多个线程安全共享数据所有权
- 跟踪有多少线程在使用数据
- 当最后一个使用者退出时自动清理
这俩个东东的使用方法和 Rc<T> 、RefCell<T> 一样。
补充:
- mutex离开作用域后会自动释放锁
- 锁在被释放之后才可以被申请到
- mutex申请锁时是阻塞运行的,直到拿到锁为止
使用方式
rust
// 1. 创建受保护数据
let data = Arc::new(Mutex::new(共享值));
// 2. 克隆指针供线程使用
let thread_data = Arc::clone(&data);
// 3. 在线程中获取锁
let mut guard = thread_data.lock().unwrap();
// 4. 操作数据
*guard = 新值;
// 5. 自动释放锁(离开作用域时)例程
多个线程同时更新同一个银行账户余额的场景:
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 创建共享银行账户,初始余额100
// Arc = Atomic Reference Count(原子引用计数),允许跨线程共享所有权
// Mutex = Mutual Exclusion(互斥锁),保证同时只有一个线程访问数据
let account = Arc::new(Mutex::new(100)); // 账户余额: 100元
println!("初始余额: {}", account.lock().unwrap());
// 创建线程句柄存储
let mut handles = vec![];
// 模拟5个ATM同时操作账户
for i in 0..5 {
// 克隆账户的智能指针(增加引用计数)
let acc_clone = Arc::clone(&account);
// 每个ATM线程存入不同金额
let handle = thread::spawn(move || {
// 存款金额:第1个ATM存10元,第2个存20元...
let deposit_amount = (i + 1) * 10;
// 获取锁(阻塞直到获得访问权)
let mut balance = acc_clone.lock().unwrap();
// 安全更新余额
*balance += deposit_amount;
println!("ATM{}存入: {}元", i, deposit_amount);
// 锁在作用域结束时自动释放
});
handles.push(handle);
}
// 等待所有ATM操作完成
for handle in handles {
handle.join().unwrap();
}
// 获取最终余额
let final_balance = account.lock().unwrap();
println!("最终余额: {}元", *final_balance);
// 预期结果: 100 + (10+20+30+40+50) = 250元
}Send、Sync trait
"线程安全"指特定数据可以被多个并发线程安全使用。实现了Send、Sync的类型自带线程安全。
更确切地说,这俩是标记类 trait,位于std::marker,类型不用实现具体代码,这更像是语言特性,不像标准库提供的其他东西。
- send 可以将所有权在线程间传送
- sync 可以直接在多个线程中直接使用引用访问
分开 Send 和 Sync 特性的原因:一个类型可能是其中之一,两者都是,或两者都不是
- Rc<T>:既不是 Send 也不是 Sync
- RefCell<T>:是Send(如果T是Send),但不是 Sync·Mutex<T>:是Send也是 Sync,可用于多线程共享访问
- MutexGuard<'a,T>:是Sync(如果T是Sync),但不是 Send