28_rust_无畏并发

无畏并发

Concurrent:程序不同部分之间独立执行;
Parallel:程序不同部分同时运行。

rust无畏并发:允许编写没有细微Bug的代码。并在不引入新Bug的情况下易于重构。这里所说的“并发”泛指concurrent和parallel。

使用线程同时运行代码

1:1模型:实现线程的方式:通过调用OS的API创建线程。只需较小的运行时。
M:N模型:语言自己实现的线程(绿色线程),需要更大的运行时。
rust:需要权衡运行时的支持,希望尽可能小的运行时支持。rust标准库仅提供1:1模型的线程。

通过spawn创建新线程
通过thread::spawn函数可创建新线程,参数是一个闭包(即在新线程里运行的代码)。
thread::sleep可让当前线程暂停执行。

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
      for i in 1..10 {
          println!("spawn thread {}", i);
          thread::sleep(Duration::from_millis(1));
      }
    });
    for i in 1..5 {
        println!("main thread {}", i);
        thread::sleep(Duration::from_millis(1));// sleep 1ms
    }
}
/* 输出结果
main thread 1
spawn thread 1
main thread 2
spawn thread 2
spawn thread 3
main thread 3
main thread 4
spawn thread 4
*/

可看出,主线程结束时,子线程也结束。
通过join Handle来等待所有线程完成
thread::spawn函数返回值类型时JoinHandle,JoinHandle持有值的所有权。调用join方法,可等待对应其它线程完成。

  • join方法:调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结。
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
      for i in 1..10 {
          println!("spawn thread {}", i);
          thread::sleep(Duration::from_millis(1));
      }
    });
    for i in 1..5 {
        println!("main thread {}", i);
        thread::sleep(Duration::from_millis(1));// sleep 1ms
    }
    handle.join().unwrap(); // 会阻止主线程执行,直到子线程结束 
}
/*输出
main thread 1
spawn thread 1
spawn thread 2
main thread 2
spawn thread 3
spawn thread 4
main thread 3
main thread 4
spawn thread 5
spawn thread 6
spawn thread 7
spawn thread 8
spawn thread 9
*/

如果将handle.join().unwrap()提前到主函数的打印之前,可看到主函数的打印会在子线程执行完之后,join会阻止主线程执行。

使用move闭包
move闭包通常和thread::spawn函数一起使用,允许使用其它线程的数据,创建线程时,把值的所有权从一个线程转移到另一个线程。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    // let hd = thread::std::thread::spawn(|| {
    //     println!("v={:?}", v); // 这里使用v若没获得所有权,则报错,因为闭包的生命周期比v更长
    // });
    let hd = thread::std::thread::spawn(move || {
        println!("v={:?}", v); // 通过move获得生命周期
    });
    // drop(v); // 如果使用drop手动释放v,则报错,因为v的所有权已转入闭包
    hd.join().unwrap();
}

使用消息传递跨线程传递数据

消息传递:线程(或Actor)通过彼此发送消息(数据)进行通信。当前很流行的技术,且能保证安全并发。
rust: Channel(标准库提供)

Channel

  • Channel包含发送端和接收端
  • 调用发送端方法发送数据
  • 接收端会检查和接收收到的数据
  • 若发送端或接收端任意一端被丢弃,则Channel“关闭”

创建Channel
使用mpsc::channel函数创建Channel

  • npsc表示multiple producer, single consumer(多个生产者,一个消费者)
  • 返回一个tuple(元组),元素分别是发送端、接收端
use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel(); //创建channel

    let hd = thread::spawn(move || { //必须获得所有权才能发消息
        let v = String::from("send in thread");
        tx.send(v).unwrap(); // send返回的是Result<T,E>,简单使用unwrap处理如果返回错误则panic
    });
    let recv = rx.recv().unwrap(); // 同步接收消息,会阻塞在recv函数,返回值是Result<T,E>
    println!("{}", recv);
}

发送端send方法

  • 参数:待发数据
  • 返回值: Result<T, E>,若有问题(如接收端已被丢弃),则返回一错误

接收端方法

  • recv()方法:会阻塞当前线程执行,直到channel中有值传入,当有值传入,则返回Result<T,E>,当发送端关闭则收到一错误。
  • try_recv()方法:不阻塞线程,调用后立即返回Result<T, E>,若收到数据则返回OK及数据,否则返回错误。通常使用循环调用来检查结果。

channel和所有权转移:所有权在消息传递中非常重要,能够保证编写安全、并发的代码。
发送多个值的例子:

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    let hd = thread::spawn(move || {
        let vs = vec![String::from("msg1"), String::from("msg2"), String::from("msg3"),];
        for v in vs {
            tx.send(v).unwrap();
            thread::sleep(Duration::from_millis(300));
        }
    });
    for recv in rx { //免去写recv函数
        println!("{}", recv);
    }
}

通过克隆创建多个发送者

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    let tx1 = mpsc::Sender::clone(&tx); // 克隆一个发送端
    thread::spawn(move || {
        let vs = vec![String::from("msg1"), String::from("msg2"), String::from("msg3"),];
        for v in vs {
            tx.send(v).unwrap();
            thread::sleep(Duration::from_millis(300));
        }
    });
    thread::spawn(move || {
        let vs = vec![String::from("tx1 msg1"), String::from("tx1 msg2"), String::from("tx1 msg3"),];
        for v in vs {
            tx1.send(v).unwrap();
            thread::sleep(Duration::from_millis(300));
        }
    });
    for recv in rx {
        println!("{}", recv); // 同时收到两个发送端信息
    }
}
/*输出结果
msg1
tx1 msg1
msg2
tx1 msg2
msg3
tx1 msg3
*/

共享状态的并发

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(3); // 创建一个锁,保护对象是3
    {
        let mut n = m.lock().unwrap(); // 获取锁,返回值LockResult<MutexGuard<'_, T>>
        *n = 5;
    } // 走完作用域,自动释放锁
    println!("{:?}", m);
}

多线程共享Mutex<T>

use std::sync::Mutex;
use std::thread;
fn main() {
    let cnt = Mutex::new(0); // 创建mutex,为计数器,初始值为0
    let mut hds = vec![]; // 创建vector,存放handle
    for _ in 0..10 { // 创建10个线程,将handle存入vector
        let hd = thread::spawn(move || { // 每个线程的目的是获取互斥锁,然后修改一次锁的值,且希望在离开作用域时将锁自动释放
            let mut n = cnt.lock().unwrap(); // 但此时这里创建了多个线程,创建第一个线程时move了所有权,第二次创建时就会报无法获取所有权的错误
            *n += 1;
        });
        hds.push(hd);
    }
    for hd in hds {
        hd.join().unwrap(); // 等待所有线程结束
    }
    println!("{}", *cnt.lock().unwrap());
}
// 编译报错 borrow of moved value: `cnt`

上面代码的报错,是因创建了多个线程,创建第一个线程时move了所有权,第二次创建时就会报无法获取所有权的错误,这里需要使用多线程的多重所有权。
使用Arc<T>进行原子引用计数,解决并发场景下多重所有权的问题。
Arc<T>Rc<T>类似,可用于并发场景,其中A是atomic(原子的)的缩写。这两者的API相同,但相比Rc需要牺牲一定的性能作为代价,所以代码设计时不会让所有基础类型设计成原子的,标准库类型也默认不使用Arc<T>

use std::sync::{Mutex, Arc}; // 引入Arc
use std::thread;
fn main() {
    let cnt = Arc::new(Mutex::new(0)); // 创建mutex,为计数器,初始值为0,使用Arc智能指针
    let mut hds = vec![]; // 创建vector,存放handle
    for _ in 0..10 { // 创建10个线程,将handle存入vector
        let cnt = Arc::clone(&cnt); // 每次让引用计数+1
        let hd = thread::spawn(move || { // 每个线程的目的是获取互斥锁,然后修改一次锁的值,且希望在离开作用域时将锁自动释放
            let mut n = cnt.lock().unwrap(); // 解决多重所有权问题
            *n += 1;
        });
        hds.push(hd);
    }
    for hd in hds {
        hd.join().unwrap(); // 等待所有线程结束
    }
    println!("{}", *cnt.lock().unwrap()); // 最后打印结果10
}

Mutex<T> + Arc<T>RefCell<T> + Rc<T>

  • Mutex<T>提供了内部可变性。和Cell家族一样
  • 使用RefCell<T>改变Rc<T>所指向的内容
  • 使用Mutex<T>改变Arc<T>所指向的内容
  • RefCell<T>有可能导致内存泄露的问题,Mutex<T>则有死锁风险

通过Send和Sync Trait扩展并发

rust语言并发特性较少,上面并发特性皆来自标准库,而不是语言本身,rust也可自己实现并发,其两个标签trait:(称之为标签trait是因其未实现任何方法):

  • std::marker::Sync和std::marker::Send

std::marker::Send

  • 实现Send trait的类型可在线程间转移所有权
  • rust中几乎所有类型都实现了Send,但Rc<T>没有实现Send,所以它只用于单线程
  • 任何完全由Send类型组成的类型也被标记为Send
  • 除了原始指针外,几乎所有基础类型都是Send

std::marker::Sync

  • Sync允许从多线程访问数据
  • 实现Sync的类型可安全被多个线程引用
  • 如果T是Sync,那么&T就是Send,引用可被安全的送往另一个线程
  • 基础类型都是Sync
  • 完全由Sync类型组成的类型也是Sync,另外Rc<T>RefCell<T>Cell<T>家族也不是Sync,Mutex<T>是Sync的

注:手动实现Send和Sync很难保证安全的

posted @ 2023-11-18 17:00  00lab  阅读(27)  评论(0编辑  收藏  举报