Rust 多线程的使用

212 阅读3分钟

1. 线程的创建

use std::thread;
use std::thread::sleep;
use std::time;
// 通过 thread 库来创建线程
fn main() {

    for i in 0..5 {
        println!("main: {}", i);
    };

    let t1 = thread::spawn(|| {
        for i in 0..10 { 
            sleep(time::Duration::from_millis(100));
            println!("thread: {}", i);
        }
    });

    t1.join().unwrap();
}

// thread::spawn() 函数返回的是 pub struct JoinHandle<T>(JoinInner<'static, T>);

JoinHandle<T> 的主要功能

  1. 等待线程完成JoinHandle<T> 提供了 join() 方法,该方法会阻塞当前线程,直到关联的线程完成执行。这确保了主线程可以等待子线程完成,从而避免了资源的竞争和数据的不一致。
  2. 获取线程返回值: 当线程完成执行时,JoinHandle<T> 可以捕获该线程的返回值。这是通过泛型参数 T 来实现的,它代表了线程的返回值类型。通过调用 join() 方法,可以获取到线程的返回值。
  3. 错误处理: 如果线程在执行过程中出现了 panic,JoinHandle<T> 会捕获该异常并将其转换为 std::thread::Result<T> 类型的错误。在调用 join() 方法时,可以处理这个错误,从而确保程序的健壮性。

2. 线程之间共享只读数据

// 1. 对于实现了 Copy trait 的数据,只需要借助 move 关键字,便可在多线程之间共享它
fn main() {
    let data = 66;
    for i in 0..5 {
        thread::spawn(move || {
            println!("{} : {:?}", i, data);
        })
        .join()
        .unwrap();
    }

    println!("{:?}", data);
}
// 2. 对于没有实现 Copy trait 的数据,由于第一次 move 后所有权被转移到了线程内。如果要实现多线程共享同一份资源,有 2 种方式:
// * 为每个线程重新 clone 一份数据
// * 利用 Arc clone 引用,让多个引用指向同一份实例

fn main() {
    let data = vec![1, 2, 3];
    let data2 = Arc::new(vec![5, 6, 7]);

    for i in 0..5 {
        let data = data.clone(); // 每个线程都 clone 一份数据,相当于堆上会有 5 份数据
        let data2 = data2.clone(); // 每个线程都 clone 一份引用,堆上只会有 1 份数据,有 5 个引用都指向这一份数据
        thread::spawn(move || {
            println!("{} : {:?}", i, data);  
            println!("{} : {:?}", i, data2);
        })
        .join()
        .unwrap();
    }
}

3. 线程之间共享可写数据

主要讲一下 clone 多份引用的时候怎么去更改那一份单独的数据。

// 通过 Mutex 智能指针,能够为单独的那一份数据加锁,且离开线程后由于 Drop tait 的作用,会自动解锁
fn main() {
    let data = Arc::new(Mutex::new(vec![5, 6, 7]));

    for i in 0..5 {
        let data = data.clone(); 
        thread::spawn(move || {
            data.lock().unwrap().push(i);
            println!("{} : {:?}", i, data.lock().unwrap());  
        })
        .join()
        .unwrap();
    }

    println!("{:?}", data.lock().unwrap()); // [5, 6, 7, 0, 1, 2, 3, 4]
}

4. 线程之间通信

线程之间通信主要是根据通道来实现的 std::sync::mpsc::channel

fn main() {

    // s 和 r 的模型是多对一的,即多个生产者一个消费者。 所以 sender 上是有 clone 的方法的,而 receiver 是没有的。
    // PS: 只针对标准库里的 channel, 其他库可能会有其它的关系
    let (sender, receiver) = channel();

    for _i in 0..10 {
        let s = sender.clone();
        thread::spawn(move || {
            for i in 0..10 {
                sleep(time::Duration::from_millis(100));
                // 巧妙在于,把 sender move 进来后,线程走完,sender 会被自动回收,
                // 而 receiver 接收不到数据,就会返回 Result<_, Error>
                s.send(i).unwrap();
            }
        });

    }

    // receiver 的结束方式是所有的 sender 都被销毁,就会返回 error
    // 此时因为用了 clone,在线程里销毁了所有 clone 的,而最外面的 sender 没有 move 进去,
    // sender 没有全部销毁,此时需要调用一下 drop
    drop(sender);

    while let Ok(a) = receiver.recv()  {
        println!("{}", a);
    }
}

5. 总结

Rust 由于其所有权的特性,在多线程操作一些没有实现 Copy trait 的数据时需要借用一些智能指针来实现。

Rust 多线程通信借用的 Go 的方法 “不要通过共享内存来通信,而应该通过通信来共享内存”。