Rust语言

关注公众号 jb51net

关闭
首页 > 软件编程 > Rust语言 > Rust使用线程同时运行代码

Rust如何使用线程同时运行代码

作者:Hello.Reader

Rust使用1:1线程模型,通过std::thread::spawn创建线程,返回JoinHandle用于等待线程完成,闭包默认借用外部变量,使用move关键字转移所有权,多线程共享数据时需使用并发原语,如Mutex、RwLock、Arc等,以避免竞态条件

一、Rust 的线程模型

Rust 标准库使用的是 1:1 的线程模型,即每一个语言层的线程都对应一个操作系统线程。Rust 中通过标准库提供的 std::thread 模块来创建、管理线程。

当然,也有一些第三方库会采用不同的线程模型,或者利用异步(async)机制来实现并发(比如 Rust 的 async/await 机制),在面对具体需求时可以根据实际情况做选择。

二、创建线程:thread::spawn

要在 Rust 中创建一个新线程,可以使用 thread::spawn 函数,并向它传递一个闭包(closure)。闭包中包含需要在线程中执行的代码。

例如:

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

fn main() {
    // 使用 thread::spawn 创建新的线程
    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));
    }
}

在上面的例子里,我们在子线程中打印数字,同时在主线程中也打印数字。由于操作系统会对线程进行调度,输出的顺序无法完全预测。可能主线程先打印,也可能子线程先打印,或者两者交错执行。

需要注意的是,当主线程结束时,所有通过 spawn 创建的子线程会被强制终止,即使子线程还没有执行完。

三、等待线程完成:JoinHandle 与 join

如果希望确保子线程的代码一定会执行完,那么就需要在主线程结束前等待子线程。thread::spawn 的返回值是一个 JoinHandle,可以用它来调用 join 方法,阻塞(block)当前线程,直到对应的子线程执行完成。

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));
    }

    // 调用 join,阻塞主线程,直到子线程结束
    handle.join().unwrap();
}

当我们在主线程中调用 handle.join() 时,主线程会暂停执行,直到子线程完成工作。这样就能确保在程序退出前,所有线程都能顺利完成执行。

如果把 join 放在主线程的循环之前,那么主线程会先等待子线程结束,才会进行自身的打印操作——这样就不会再看到主线程与子线程的输出交错了。

四、move 闭包与线程

多线程编程中常常需要在线程间传递数据或访问主线程中的变量。

在 Rust 中,如果一个闭包想要捕获外部变量,就要考虑该变量的所有权或引用生命周期问题。

4.1.问题场景

如下示例所示,如果我们在主线程中创建一个向量 v,然后在子线程中直接打印这个向量,就会出错:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    // ...
    handle.join().unwrap();
}

编译时,Rust 会提示闭包捕获的是对 v 的引用,但无法保证在子线程运行时 v 依旧有效:主线程可能在子线程使用 v 之前就结束了,让 v 不再有效,从而导致潜在的悬垂引用(dangling reference)。

4.2.使用 move 关键字

为了解决这个问题,需要在闭包前面加上 move 关键字,这样可以把闭包中用到的外部数据移动到闭包的所有权中。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

在这里,move 会把 v 的所有权从主线程转移到子线程,从而保证子线程在使用 v 时不会遇到生命周期问题。不过需要注意的是,这样一来,主线程就不能再使用 v 了,因为所有权已经被移动出去了。

4.3.不能与 drop 共用所有权

如果尝试同时在主线程中显式调用 drop(v) 并且在子线程中使用 v,无论有没有用 move,都行不通。Rust 的所有权规则会保证同一份数据不会被多次释放或引用到失效的数据。所以,在设计多线程逻辑时,需要明确划分数据的所有权与生命周期,以避免死锁、竞态条件或悬垂引用等问题。

五、小结

  1. Rust 标准库中的线程模型:Rust 使用一对一(1:1)模型,每个语言线程对应一个系统线程。
  2. 创建线程:使用 thread::spawn 来创建子线程,传入一个闭包作为要执行的代码。
  3. 线程同步:通过返回的 JoinHandle 调用 join,可以阻塞主线程并等待子线程完成执行。
  4. 所有权与生命周期:使用 move 关键字将闭包所需的变量从主线程移动到子线程,从而避免引用冲突或无效引用。
  5. 小心共享数据:当多个线程需要同时访问或修改同一份数据时,需要使用安全的并发原语(例如 MutexRwLockArc 等),否则会出现竞态条件。

在 Rust 中编写并发程序时,我们需要充分利用所有权与借用检查器提供的安全保障,同时对多线程逻辑进行精心设计。尽管多线程编程能带来性能上的提升,但也应关注潜在的风险,并通过 Rust 的工具链和语言特性来尽量减少错误,写出更安全、更可靠的并发应用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文