十四、Rust 并发编程
- 并发编程
- 程序的不同部分相互独立执行
- 并行编程
- 程序不同部分于同时执行
- Go 的一个口号:不要通过共享内存来通讯,而是通过通讯来共享内存
1. 多线程
-
多线程编程的常见问题
- 资源竞争:多个线程以不同的顺序访问数据或资源
- 资源死锁:多个线程互相等待其他线程释放资源
- 容易发生只在特定情况下才会出现的难以稳定重现的 bug
-
创建新线程
// 来自官方教程的一个示例 use std::thread; use std::time::Duration; fn main() { // 创建一个新线程,以闭包的形式将需要执行的代码传递进去 // 特点:当主线程结束时,新线程也会结束 let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); // 强制停止一段时间 thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } // 等待线程执行完成:阻塞当前线程直到 handle 所代表的线程结束 handle.join().unwrap(); } -
move闭包-
经常与
thread::spawn一起使用,因为它允许我们在一个线程中使用另一个线程的数据// 来自官方教程的一个示例 use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {:?}", v); }); // 这里及以后的代码都不能再访问到变量 v handle.join().unwrap(); }
-
2. 线程间消息传递
-
在 Rust 中,可以通过 通道 (Channel) 实现消息传递并发
-
通道
- 发送者 (transmitter)
- 接收者 (receiver)
- 当发送者和接收者的任意一方被丢弃时,就可以认为通道被关闭了
-
示例代码
// 本示例改编自官方教程的示例 use std::sync::mpsc; // mpsc: multiple producer, single consumer. 多生产者,单消费者 use std::thread; use std::time::Duration; pub fn channel_test() { single_msg(); println!("------ separator ------"); multi_msg(); println!("------ separator ------"); multi_tx_msg(); } fn single_msg() { // 历时原因,tx 表示发送者的简写,rx 表示接收者的简写 let (tx, rx) = mpsc::channel(); thread::spawn(move || { let msg = String::from("hi"); // 如果接收端已经被丢弃了,将没有发送值的目标,这个方法会返回错误,需要单独处理 // 在这里如果发送失败,就会产生 panic tx.send(msg).unwrap(); // 注意:send 函数会获取其参数的所有权,并移动这个值归接收者所有 // 因此从这里往下的作用域内都不可访问 msg }); // recv: 阻塞主线程执行,直到从通道中接收一个值 // try_recv: 不阻塞主线程执行,立刻返回一个 Result<T, E>:Ok 值包含可用的信息,Err 值代表此时没有任何消息 let received = rx.recv().unwrap(); println!("Got: {}", received); } fn multi_msg() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let msgs = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for msg in msgs { tx.send(msg).unwrap(); thread::sleep(Duration::from_secs(1)); } }); // rx: Receiver<T> 有实现 IntoIterator, 因此可以用 for..in 迭代遍历 // 当消息通道被关闭时,这个迭代器也会结束 let mut msgs: Vec<String> = vec![]; for received in rx { println!("...receive a message..."); msgs.push(received); } println!("received {:?}", msgs); } fn multi_tx_msg() { let (tx, rx) = mpsc::channel(); // 线程 1 的消息 let tx1 = mpsc::Sender::clone(&tx); thread::spawn(move || { let msgs = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in msgs { tx1.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); // 线程 2 的消息 thread::spawn(move || { let msgs = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in msgs { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); let mut msgs: Vec<String> = vec![]; for received in rx { println!("...receive a message..."); msgs.push(received); } println!("received {:?}", msgs); }
3. 线程状态共享
-
互斥器 (mutex, mutual exclusion)
- 特性:一次只允许一个线程访问数据
- 锁:一种数据结构,是互斥器的一部分,记录了谁有数据的排他访问权
-
原子引用计数
Arc<T>- 工作起来类似原始类型,且可以安全地在线程之间共享
- 没有默认启用这个特性的原因:线程安全带有额外的性能开销
-
示例代码
// 改编自官方教程的一个示例 use std::sync::{Arc, Mutex}; use std::thread; pub fn shared_state_test() { first_in(); println!("------ separator ------"); multi_thread_shared_state(); } fn first_in() { let m = Mutex::new(5); { // 通过 lock 方法来获取锁,这个方法会阻塞当前线程,直到当前线程拥有锁之后才会继续执行 // 如果拥有锁的线程 panic 了,任何其他线程都无法再获取锁,也会导致当前线程的这个 lock 调用失败 // 通过 unwrap 来直接对获取到的锁解包,如果解包失败,当前线程就会 panic let mut num = m.lock().unwrap(); // MutexGuard 实现了 Deref trait,所以可以直接通过解引用操作来访问它内部的值 *num = 6; // MutexGuard 会在作用域结束之后自动释放锁 // 因为它实现里的 Drop trait,并在 drop 中实现了释放锁的逻辑 } println!("m = {:?}", m); } fn multi_thread_shared_state() { // 创建一个共享状态 let counter = Arc::new(Mutex::new(0)); // 用于保存每一个线程的 handle let mut handles = vec![]; for index in 0..10 { // 获取一个共享状态的引用拷贝 let counter = Arc::clone(&counter); // 创建新线程 let handle = thread::spawn(move || { // 通过共享状态的引用拷贝,来获取共享状态的值 let mut num = counter.lock().unwrap(); // 通过解引用,来对值进行操作 *num += 1; println!("current thread: {}", index + 1); }); // 把这个线程的 handle 记下来 handles.push(handle); } // 等待每一个线程执行完 for handle in handles { handle.join().unwrap(); } // 查看最终结果 println!("Result: {}", *counter.lock().unwrap()); } -
一点总结
Mutex<T>提供了内部可变性,类似于RefCell<T>- 可以利用
RefCell<T>来改变Rc<T>中的内容 - 相似的,可以利用
Mutex<T>来改变Arc<T>中的内容 - 两个
Rc<T>互相引用会导致内存泄露,此时是用Weak<T>来解决 - 两个
Mutex<T>互相引用也会导致死锁,从而造成内存泄露- 场景:一个操作需要锁住两个资源,而两个线程各持了一个锁
- 此时没有办法解决
4. 可扩展并发
4.1 Send trait
- 一个标记 trait,无需实现任何方法
- 作用:表明类型的所有权可以在线程间传递
- 几乎所有的 Rust 类型都是可
Send的,以下类型例外:Rc<T>RefCell<T>Cell<T>系列
- 任何完全由
Send的类型组成的类型也会自动被标记为Send - 除了裸指针外,几乎所有基本类型都是可
Send的
4.2 Sync trait
- 一个标记 trait,无需实现任何方法
- 作用:表明类型允许安全地在多个线程中拥有其值的引用
- 换句话说:对于任意类型
T,如果&T是可Send的,则T是Sync的
- 换句话说:对于任意类型
- 非
Sync的类型Rc<T>RefCell<T>Cell<T>系列
- 注意:
- 由
Send和Sync的类型组成的类型,其本身就具有这两个特性 - 手动实现这两个 trait 涉及到编写不安全的 Rust 代码!
- 由