十四、Rust 并发编程

258 阅读6分钟

十四、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 的,则 TSync
  • Sync 的类型
    • Rc<T>
    • RefCell<T>
    • Cell<T> 系列
  • 注意:
    • SendSync 的类型组成的类型,其本身就具有这两个特性
    • 手动实现这两个 trait 涉及到编写不安全的 Rust 代码!