Rust(十六)-无畏并发

271 阅读6分钟

并发

  • concurrent: 程序的不同部分之间独立的执行
  • parallel: 程序的不同部分同时运行
  • Rust 无畏并发:允许你编写没有细微bug的代码,并在不引入新bug的怀况下易于重构
  • 注意:本课程中的“并发”泛指concurrent和parallel

使用线程同时运行代码

  • 进程与线程
    • 在大部分os里,代码运行在进程(process)中,os同时管理多个进程。
    • 在你的程序里,各独立部分可以同时运行,运行这些独立部分的就是线程(thread)
    • 多线程运行
      • 提升性能表现
      • 增加复杂性:无法保障各线程的执行顺序
  • 多线程可导致的问题
    • 竞争状态,线程以不一致的顺序访问数据或资源
    • 死锁,两个线程彼此等待对方使用完所持有的资源,线程无法继续
    • 只在某些情况下发生的bug,很难可靠地复制现象和修复
  • 实现线程的方式
    • 通过调用os的api来创建线程:1:1模型
      • 需要较小的运行时
    • 语言自己实现的线程(绿色线程):M:N模型
      • 需要更大的运行时
    • RUST: 需要权衡运行时的支持
    • Rust标准库仅提供1:1模型的线程
  • 通过join Handle来等所有线程的完成
    • thread::spawn函数的返回值类型是 joinHandle
    • joinhandle 持有值的所有权
      • 调用共join方法,可以等待对应的其它线程的完成
    • join方法:调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结
  • 使用move闭包
    • move闭包通常和thread::spawn函数一起使用,它允许你使用其它线程的数据
    • 创建线程时,把值的所有权从一个线程转移到另一个线程

消息传递

  • 一种很流行且能保证安全并发的技术就是:消息传递
    • 线程(或actor)通过彼此发送消息(数据)来进行通信
    • Go语言的名言:不要用共享内存来通信,要用通信来共享内存。
    • Rust:Channel(标准库提供)
  • channel
    • chnnel包含:发送端、接收端
    • 调用发送端的方法,发送数据
    • 接收端会检查和接收到达的数据
    • 如果发送端、接收端中任意一端被丢弃了,那么channel就“关闭”了
  • 创建channel
    • 使用mpsc::channel函数来创建channel
      • mpsc表示multiple producer, single consumer(多个生产者、一个消费者)
      • 返回一个tuple(元组): 里面元素分别是发送端、接收端
      use std::sync::mpsc;
      use std::thread;
      
      fn main() {
          let (tx, rx) = mpsc::channel();
      
          thread::spawn(move || {
              let val = String::from("hi");
              tx.send(val).unwrap();
              println!("val is {}", val);
          });
      
          let received = rx.recv().unwrap();
          println!("Got: {}", received);
      }
      
  • 发送端的send方法
    • 参数:想要发送的数据
    • 返回:Result<T,E>
      • 如果有问题(例如接收端已经被丢弃),就返回一个错误
  • 接收端的方法
    • recv方法:阻止当前线程执行,直到channel中有值被送来
      • 一旦有值收到,就返回Result<T,E>
      • 当发送端关闭,就会收到一个错误
    • try_recv方法:不会阻塞
      • 立即返回Result<T,E>:
        • 有数据达到:返回ok,里面包含着数据
        • 否则,返回错误
      • 通常会使用循环调用来检查try_recv的结果
  • channel和所有权转移
    • 所有权在消息传递中非常重要:能帮你编写安全、并发的代码
     use std::sync::mpsc;
     use std::thread;
     use std::time::Duration;
    
     fn main() {
         let (tx, rx) = mpsc::channel();
    
         thread::spawn(move || {
             let vals = vec![
                 String::from("hi"),
                 String::from("from"),
                 String::from("the"),
                 String::from("thread"),
             ];
    
             for val in vals {
                 tx.send(val).unwrap();
                 thread::sleep(Duration::from_secs(1));
             }
         });
    
         for received in rx {
             println!("Got: {}", received);
         }
     }
    

共享状态的并发

  • 使用共享来实现并发
    • Go语言的名言:不要用共享内存来通信,要用通信来共享内存。
    • Rust支持通过共享状态来实现并发
    • Channel类似单所有仅:一旦将值的所有权转移至channel,就无法使用它了
    • 共享内存并发类似多所有权:多个线程可以同时访问同一块内存
  • 使用mutex来每次只允许一个线程来访问数据
    • Mutex是mutual exclusion(互斥锁)的简写
    • 在同一时刻,Mutex只允许一个线程来访问某些数据
    • 想要访问数据
      • 线程必须首先获取互斥锁(lock)
        • lock数据结构是mutex的一部分,它能跟踪谁对数据拥有独占访问权
      • mutex通常被描述为:通过锁定系统来保护它所持有的数据
  • mutex 的两条规则
    • 在使用数据之前,必须尝试获取锁(lock)
    • 使用完mutex所保护的数据,必须对数据进行解锁,以便其它线程可以获取锁
  • mutex< T >的api
    • 通过mutex::new(数据)来创建mutex< T >
      • mutex< T >是一个智能指针
    • 访问数据前,通过lock方法来获取锁
      • 会阻塞当前线程
      • lock可能会失败
      • 返回的是mutexGuard(智能指针,实现了deref 和 drop)
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {:?}", m);
}

  • 多线程的多重所有权
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

使用Arc< T >来进行原子引用计数

  • Arc< T >和Rc< T >类似,它可以用于并发情景
    • A: atomic,原子的
  • 为什么所有的基本类型不是原子的,为什么标准库类型不默认使用Arc< T >
    • 需要性能作为代价
  • Arc< T >和Rc< T > 的 api是相同的

RefCell< T >/Rc< T > vs Mutex< T >/Arc< T >

  • Mutex< T > 提供了内部可变性,和Cell家族一样
  • 我们使用RefCell< T >来改变Rc< T >里面的内容
  • 我们使用Mutex< T >来改变Arc< T >里面的内容
  • 注意:Mutex< T >有死锁风险

通过send和sync trait来扩展开发

  • rust语言的并发特性比较少,目前讲的并发特性都自于标准库(而不是语言本身)
  • 无需局限于标准库的并发,可以自己实现并发
  • 但在rust语言中有两个并发概念
    • std::marker::sync和std::market::send这两个trait

send: 允许线程间转移所有权

  • 实现send trait 的类型可在线程间转移所有权
  • Rust中几乎所有的类型都实现了send
    • 便rc< t >没有实现send,它只用于单线程情景
  • 任何完全由send类型组成的类型也被标记为send
  • 除了原始指针之外,几乎所有的基础类型都是send
  • sync:允许从多线程访问
    • 实现sync的类型可以安全的被多个线程引用
    • 也就是说:如果T是sync, 地么&T就是Send
      • 引用可以被安全的送往另一个线程
    • 基础类型都是sync
    • 完全由sync类型组成的类型也是sync
      • 但,Rc< T >不是sync的
      • RefCell和cell家话也不是sync的
  • 手动实现send和sync是不安全的