【Rust 进阶教程】 04 并发编程

264 阅读5分钟

0x00 开篇

到目前为止,我们所写的程序都是单线程运行的,然而在实际开发的工作环境中,我们经常会遇见多任务处理,比如一个下载任务。当下载任务开启时,我们还需要处理其它事情,这时单线程的任务并不满足我们的需求。从本篇文章开始,我们将开启并发编程的学习。

0x01 线程的创建与使用

创建线程通常被称为 产生线程 (spawning thread)。我们使用 std::thread::spawn 函数,该函数接收一个 FnOnce 型的闭包。源码如下:

// std::thread::spawn
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    Builder::new().spawn(f).expect("failed to spawn thread")
}

产生线程的时候,这些线程被认为是从父线程复刻(fork)出来的,这也就是我们常说的 fork 一个线程。来看下示例代码:

fn main() {
    let duration = Duration::from_millis(3000);
    
    thread::spawn(move || {
        thread::sleep(duration);
        println!("线程1 执行完成");
    });

    thread::spawn(move || {
        thread::sleep(duration);
        println!("线程2 执行完成");
    });
    println!("程序 执行结束");
}
// 运行结果
// 程序 执行结束

上面的代码是创建了两个线程,在每个线程里面 休息3秒钟 后输出 线程x 执行完成,最后再输出 程序 执行结束。运行后发现,程序仅打印了 程序 执行结束。这是由于,我们在创建线程之后,父线程的程序继续执行,并没有去“理会”子线程的运行结果。当父线程都已经执行结束了,子线程还没有执行或者执行到一半。在实际操作中,我们还需要将线程连接起来。我们使用 join 将线程捆绑合并在一起。join 函数的作用就是等待另一个线程执行结束。修改后的代码如下:

fn main() {
    let duration = Duration::from_millis(3000);
    let handle1 = thread::spawn(move || {
        thread::sleep(duration);
        println!("线程1 执行完成");
    });

    let handle2 = thread::spawn(move || {
        thread::sleep(duration);
        println!("线程2 执行完成");
    });

    // 等待线程结束
    handle1.join().unwrap();
    handle2.join().unwrap();

    println!("程序 执行结束");
}
// 运行结果
// 线程1 执行完成
// 线程2 执行完成
// 程序 执行结束
spawn 为什么需要传入 FnOnce 闭包

我们产生的子线程可能会比父线程  的更久,这就意味着需要通过 move 来将其所有权转到子线程中。

0x02 跨线程共享“不可变数据”

通常情况下,我们会存在多线程使用同一个数据的场景。假设我有两个线程都需要使用一组数据,大家最先想到的就是 clone 一份数据。当然,这样做是可以解决问题的。假设这组数据非常大呢?有 10 个线程要共享呢?这就需要我前面《Rust 中级教程 第12课》所介绍的 Arc ——原子引用计数。示例代码如下:

fn main() {
	let duration = Duration::from_millis(1000);
    // 这样可以解决问题,但在数据量很大的时候,不推荐
    // let data = vec!["hello".to_string(), "rust".to_string()];

    // Arc 原子引用计数
    let data: Arc<Vec<String>> = Arc::new(vec!["hello".to_string(), "rust".to_string()]);

    // clone 仅会增加引用计数,不会真正的 clone 数据
    let handle1_data = data.clone();
    let handle1 = thread::spawn(move || {
        thread::sleep(duration);
        println!("线程1 {:?}", handle1_data);
        println!("线程1 执行完成");
    });

    // clone 仅会增加引用计数,不会真正的 clone 数据
    let handle2_data = data.clone();
    let handle2 = thread::spawn(move || {
        thread::sleep(duration);
        println!("线程2 {:?}", handle2_data);
        println!("线程2 执行完成");
    });

    // 等待线程结束
    handle1.join().unwrap();
    handle2.join().unwrap();

    println!("程序 执行结束");
}
// 程序运行结果
// 线程2 ["hello", "rust"]
// 线程2 执行完成
// 线程1 ["hello", "rust"]
// 线程1 执行完成
// 程序 执行结束

我们调用 data.clone 方法时,并不会再次创建一个 data 副本,而是增加了一次引用计数。只要有任何线程拥有 Arc<Vec<String>>,映射就不会释放,即使是父线程已经不存在了。由于 Arc 中的数据是不可变的,所以不会出现数据竞争的问题。

0x03 JoinHandle

std::thread::spawn 的返回值是 JoinHandle , 它是一个结构体,包含一个 JoinInner 类型。源码如下:

pub struct JoinHandle<T>(JoinInner<'static, T>);

/// Inner representation for JoinHandle
struct JoinInner<'scope, T> {
    native: imp::Thread,
    thread: Thread,
    packet: Arc<Packet<'scope, T>>,
}

JoinHandle 用于等待线程完成其执行并检索它所产生的结果。它里面有几个重要的方法简单介绍下:

join

要等待线程完成并获取其结果,可以调用 JoinHandle 的 join 方法(上面已经介绍过了)。该方法会阻塞当前线程,直到与 JoinHandle 关联的线程完成其执行,然后返回线程产生的结果。如果线程发生错误(panic),则 join 方法也会抛出错误(panic)。

另外子线程的返回结果也会通过 join 方法返回。示例代码如下:

fn main() {
	let duration = Duration::from_millis(1000);
    let handle = thread::spawn(move || {
        thread::sleep(duration);
        // 返回一个 String 类型
        return "hello".to_string();
    });
	
    // 父线程接收子线程运行结束的结果
    let result = handle.join().unwrap();
    println!("{}", result);
}
// 运行结果
// hello
is_finished

该方法从 Rust 1.61.0 版本变为 stable。该方法用于检查与 JoinHandle 相关联的线程是否已经执行完成。通常情况下,在调用 join 方法之前,我们可以使用 is_finished 方法来查询与 JoinHandle 对象相关联的线程是否已经完成执行。如果返回值为 true,则说明线程已经执行完成,并且可以通过调用 join 方法来等待线程退出并获取其返回值。示例代码如下:

fn main() {
	let duration = Duration::from_millis(1000);
    let handle = thread::spawn(move || {
        thread::sleep(duration);
        // 返回一个 String 类型
        return "hello".to_string();
    });
    let handle_finished = handle.is_finished();
    println!("handle_finished: {}", handle_finished);

    // 父线程接收子线程运行结束的结果
    let result = handle.join().unwrap();

    println!("{}", result);
}
// 运行结果
// handle_finished: false
// hello
thread

我们可以使用 thread 方法从 JoinHandle 中获取线程的一些信息,像线程 Id, 线程的名称等。示例代码如下:

fn main() {
    let duration = Duration::from_millis(1000);
    let handle = thread::spawn(move || {
        thread::sleep(duration);
        // 返回一个 String 类型
        return "hello".to_string();
    });

    let thread = handle.thread();
    println!("{:?}", thread);

    // 父线程接收子线程运行结束的结果
    let result = handle.join().unwrap();
    println!("{}", result);
}
// 运行结果
// Thread { id: ThreadId(2), name: None, .. }
// hello

0x04 小结

读完文章感觉怎么样呢?有没有感觉 Rust 创建一个线程很简单呢,哈哈哈。到目前为止,你基本已经对 Rust 的并发编程有初步了解了。我们将在下一篇文章继续更深入的了解并发编程。