Rust线程

并发编程(Concurrent programming)代表程序不同部分相互独立地执行,而并行(parallel programming)代表程序不同部分同时执行。

Rust提供了多种工具以符合实际情况和需求来为问题建模。

大部分现代操作系统中,已执行程序的代码在一个进程(process)中运行,操作系统会负责管理多个进程。在程序内部可以拥有多个同时运行的独立部分,称作线程(threads)。将程序拆分进多个线程可以改善性能,不过会增加复杂性。因为程序是同时运行的,所以无法保证不同线程中代码的执行顺序,这会导致以下问题:

  • 竞态条件(Race condition),当多个线程以不一致的顺序访问数据或资源
  • 死锁(Deadlock),当两个或多个线程相互等待对方释放资源
  • 只会发生在特定情况且难以稳定重现和修复的bug。

Rust标准库使用1:1线程实现,每个语言级线程使用一个系统线程。

使用spawn创建新线程🔗

为了创建一个新线程,需要调用thread::spawn函数,这个函数会获取一个闭包(closure)作为参数,这个闭包会在新线程中执行:

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

fn main() {
    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));
    }
}

这段代码在新线程和主线程中打印了一些文本,主线程结束时,新线程也会结束而不管其是否执行完毕。这个程序输出每次可能略有不同,不过大体上看起来像这样:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!

thread::sleep调用强制线程停止执行一小段时间,允许其他不同的线程运行,这些线程的运行依赖操作系统的调度。

使用join等待所有线程结束🔗

由于主线程结束,其他线程的代码甚至可能不会被执行,可以通过将thread::spawn的返回值储存在变量中来修复新建线程没有执行的问题。thread::spawn返回的是JoinHandle类型,这个一个拥有所有权的值。JoinHandle的join方法会让主线程等待新线程结束:

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

    handle.join().unwrap();
}

这段代码调用handle的join方法,这个方法会阻塞(Blocking)当前线程,直到新线程结束。这段代码产生类似下面的输出:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

如果将handle.join().unwrap();放在main中for循环之前,则会先打印出新线程的输出,再打印主线程的输出。

将move闭包与线程一同使用🔗

move关键字经常用于传递给thread::spawn的闭包。move关键字会强制闭包获取其使用的变量的所有权,这意味着这些值的所有权会从一个线程转移到另一个线程。下面是一个在主线程中创建一个vector并用于新建线程的例子:

use std::thread;

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

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

    handle.join().unwrap();
}

这里闭包会捕获v,并在新线程中访问v,但是这段代码会报错,因为Rust会尝试对v借用,由于Rust不知道新线程会持续执行多久,所以无法知晓对v的引用是否一致有效。下面是一个例子:

use std::thread;

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

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

    handle.join().unwrap();
}

如果这段代码可以运行,新线程可能根本没有机会运行,这里在新线程开始运行后主线程直接丢弃了v。

通过在闭包前增加move关键字,可以强制闭包获取所有权,而不是由Rust推断的借用,下面为修改后的代码:

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

如果在handle之前调用drop(v),则这段代码会报错,因为v的所有权已经被移动到了新线程中,主线程无法再访问v。