66、Threads(线程)

Threads(线程)

 

在开始编写多线程代码之前,让我们退一步先学习一下什么是线程,以及为什么要使用线程。

 

什么是线程?

 

线程,是由底层操作系统管理的可执行上下文。每一个线程都有自己的堆栈和指令指针。

 

一个process(进程)可以管理多个thread(线程)。这些线程共享相同的内存空间,这意味着它们可以访问相同的数据。

 

线程是一种逻辑结构。归根结底,我们一次只能在一个CPU核心上执行一组指令。

由于线程数可能比CPU内核多得多,因此操作系统的调度程序负责决定在任何特定时间运行哪个线程,并将CPU时间分配给这些线程,以最大限度地提高吞吐量和响应速度。

 

main

 

当Rust程序启动时,它会在单线程(即 main thread 主线程)上运行。该线程由操作系统创建,负责运行main函数。

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

fn main() {
    loop {
        thread::sleep(Duration::from_secs(2));
        println!("Hello from the main thread!");
    }
}

 

std::thread

 

Rust的标准库提供了一个模块,std::thread,允许我们创建和管理线程。

 

spawn

我们可以使用std::thread::spawn创建新线程,并在其上执行代码。

举个例子:

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

fn main() {
    let handle = thread::spawn(|| {
        loop {
            thread::sleep(Duration::from_secs(1));
            println!("Hello from a thread!");
        }
    });
    
    loop {
        thread::sleep(Duration::from_secs(2));
        println!("Hello from the main thread!");
    }
}

如果我们在Rust playground上运行这个程序,我们可以看到主线程和生成的线程同时运行。

 

image-20250926085536704

每个线程都独立于其他的线程工作。

 

Process termination(进程终止)

当主线程结束时,整个进程都将退出。

一个被创建的子线程会继续运行,直到它自身完成,或者主线程结束为止。

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

fn main() {
    let handle = thread::spawn(|| {
        loop {
            thread::sleep(Duration::from_secs(1));
            println!("Hello from a thread!");
        }
    });

    thread::sleep(Duration::from_secs(5));
}

在上面的示例中,我们可以看到“Hello from a thread!”信息大约被打印了五次。

 

image-20250926090036602

实际大概率是四次。

然后,主线程将结束(当main中的sleep执行完毕并调用return时),生成的线程也将终止,因为整个进程已经退出。

 

join

我们也可以通过调用spawn返回的JoinHandle上的join方法来等待生成的线程结束。

use std::thread;
fn main() {
    let handle = thread::spawn(|| {
        println!("Hello from a thread!");
    });

    handle.join().unwrap();
}

在这个例子中,主线程将等待生成的线程结束后再退出。

 

这在两个进程中引入了一种同步形式:在程序退出之前,我们肯定能看到“Hello from a thread!”信息被打印出来,因为主线程在生成的线程完成任务之前不会退出。

 

练习

 

这一节的练习当然就是让我们开发多线程程序了。题目是这样的:

// TODO: implement a multi-threaded version of the `sum` function
//  using `spawn` and `join`.
//  Given a vector of integers, split the vector into two halves and
//  sum each half in a separate thread.

// Caveat: We can't test *how* the function is implemented,
// we can only verify that it produces the correct result.
// You _could_ pass this test by just returning `v.iter().sum()`,
// but that would defeat the purpose of the exercise.
//
// Hint: you won't be able to get the spawned threads to _borrow_
// slices of the vector directly. You'll need to allocate new
// vectors for each half of the original vector. We'll see why
// this is necessary in the next exercise.
use std::thread;

// TODO: 实现一个支持多线程的 sum 函数
//  使用 spawn 和 join。
//  给定一个整数向量,将其拆分为两半,并在两个独立的线程中分别对每 half 求和。

// 注意:我们无法测试函数的具体实现方式,
// 只能验证其结果是否正确。
// 你可以通过直接返回 v.iter().sum() 来通过测试,
// 但这违背了本练习的设计初衷。

// 提示:你无法让生成的线程借用原向量的子切片。
// 你需要为原向量的每一半分配新的向量。我们将在下一个练习中了解原因。

 

跟着IDE的提示,我写的代码是这个:

pub fn sum(v: Vec<i32>) -> i32 {
    let handle = thread::spawn(move || {
        let mut sum1=0;
        for i in 0..v.len()/2 {
            sum1 += v[i];
        }
        sum1
    });
    let handle2 = thread::spawn(move || {
        let mut sum2=0;
        for i in v.len()/2..v.len() {
            sum2 += v[i];
        }
        sum2
    });
    handle.join().unwrap()+handle2.join().unwrap()
}

 

但这是不对的,看一下官方的代码:

pub fn sum(v: Vec<i32>) -> i32 {
    let mid = v.len() / 2;
    let (v1, v2) = v.split_at(mid);
    let v1 = v1.to_vec();
    let v2 = v2.to_vec();

    let handle1 = thread::spawn(move || v1.into_iter().sum::<i32>());
    let handle2 = thread::spawn(move || v2.into_iter().sum::<i32>());

    handle1.join().unwrap() + handle2.join().unwrap()
}

 

看来问题就在于两个线程同时访问了一个字段,假如我提前把Vec拷贝两份呢?

image-20250926141845963

 

果然顺利通过了,估计这个就是原题提示里面的问题:我们不能随意地让多个线程共享资源。

 

这一节的学习就先到这里了。

阅读剩余
THE END