Rust 的线程模型

线程库官方文档 https://doc.rust-lang.org/stable/std/thread/index.html

线程模型

一个正在执行的 Rust 程序由一组原生操作系统线程组成,每个线程都有自己的堆栈和本地状态。线程可以被命名,并且提供了对底层同步机制的一些内置支持。

线程间的通信可以通过通道(Rust 的消息传递类型)以及其他形式的线程同步和共享内存数据结构来实现。特别是,那些保证线程安全的类型可以轻松地通过原子引用计数容器 Arc 在线程间共享。

Rust 中的致命逻辑错误会导致线程 panic,在此期间线程会展开堆栈,运行析构函数并释放拥有的资源。虽然 panic 并非作为 “try/catch” 机制设计,但在 Rust 中仍然可以通过 catch_unwind 捕获(除非使用 panic=abort 编译)并恢复,或者通过 resume_unwind 继续传播。如果 panic 未被捕获,线程将退出,但可以通过 join 从其他线程检测到该 panic。如果主线程发生 panic 且未被捕获,应用程序将以非零退出码退出。

当 Rust 程序的主线程终止时,整个程序会关闭,即使其他线程仍在运行。然而,此模块提供了便捷的功能来自动等待线程终止(即 join)。

生成线程

可以使用thread::spawn以下函数生成一个新线程:

use std::thread;

thread::spawn(move || {
    // some work here
});

在这个例子中,生成的线程是“detached”状态,这意味着程序无法了解生成的线程何时完成或终止。

要了解线程何时完成,需要捕获JoinHandle调用所返回的对象spawn,该方法提供了一种join方法,允许调用者等待生成的线程完成:

use std::thread;

let thread_join_handle = thread::spawn(move || {
    // some work here
});
// some work here
let res = thread_join_handle.join();

join方法返回一个thread::Result,其中包含Ok(由被创建线程产生的最终值),或者如果线程发生 panic,则包含传递给panic!调用的值的Err

请注意,在创建新线程的线程与被创建的线程之间不存在父/子关系。具体来说,线程之间是平级关系,被创建的线程可能比创建它的线程存活时间更长,也可能更短,除非创建线程是主线程。

分离线程

如果连接句柄被丢弃,生成的线程将隐式分离。在这种情况下,生成的线程可能无法再被连接。程序有责任最终连接其创建的线程或将其分离;否则将导致资源泄漏。

// std/src/thread/mod.rs

spawns a new thread, returning a JoinHandle for it.
The join handle provides a join method that can be used to join the spawned thread. If the spawned thread panics, join will return an Err containing the argument given to panic!.
If the join handle is dropped, the spawned thread will implicitly be detached. In this case, the spawned thread may no longer be joined. (It is the responsibility of the program to either eventually join threads it creates or detach them; otherwise, a resource leak will result.)
.....

#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + <em>'static</em>,
    T: Send + <em>'static</em>,
{
    Builder::new().spawn(f).expect("failed to spawn thread")
}

那么“如果连接句柄被丢弃,生成的线程将隐式分离”是如何实现的呢?JoinHandle结构体中包含了imp::Thread,它直接包装了操作系统原生线程,当它离开作用域触发Drop,会调用系统线程APIlibc::pthread_detach分离原生线程。

// std/src/thread/mod.rs

struct JoinInner<<em>'scope</em>, T> {
    native: imp::Thread,
    thread: Thread,
    packet: Arc<Packet<<em>'scope</em>, T>>,
}
...
pub struct JoinHandle<T>(JoinInner<<em>'static</em>, T>);

// std/src/sys/pal/unix/thread.rs

//  上文的imp::Thread
impl Drop for Thread {
    fn <em>drop</em>(&mut self) {
        let ret = unsafe { libc::pthread_detach(self.id) };
        debug_assert_eq!(ret, 0);
    }
}

Join 线程

JoinHandle::join底层用的还是Libcpthread_join,它是 POSIX 线程库中用于线程同步的关键函数,核心作用是:阻塞调用它的线程,直到指定的目标线程终止,并获取目标线程的退出状态,同时回收该线程的资源。

// std/src/sys/pal/unix/thread.rs

pub fn join(self) {
    let id = self.into_id();
    let ret = unsafe { libc::pthread_join(id, ptr::null_mut()) };
    assert!(ret == 0, "failed to join thread: {}", io::Error::from_raw_os_error(ret));
}
  • POSIX 是一个旨在为不同Unix变体提供统一API标准的规范。它定义了操作系统应该提供的一系列接口,包括文件操作、进程控制、以及线程管理。
  • 在操作系统层面,Unix/Linux 早期只有“进程”的概念。后来为了更轻量级的并发,引入了“线程”。
  • POSIX 定义的这套线程接口被称为 Pthreads。

libc::pthread_join的核心作用:

  • 同步: 确保主线程(或调用线程)等待子线程完成工作后再继续。
  • 资源清理: 回收已终止线程的资源(如栈空间、线程描述符等),防止“僵尸线程”产生。
  • 数据传递: 提供了一种机制,让线程能够将结果传递回连接它的线程。

Pthreads

额外阅读:【博客园】关于pthread_create()和pthread_join()的多线程详解

该 API 的官方文档位于:pthread_join(3) — Linux manual page

After a successful call to pthread_join(), the caller is guaranteed that the target thread has terminated. The caller may then choose to do any clean-up that is required after termination of the thread (e.g., freeing memory or other resources that were allocated to the target thread). 

Joining with a thread that has previously been joined results in undefined behavior. 

Failure to join with a thread that is joinable (i.e., one that is not detached), produces a "zombie thread". Avoid doing this, since each zombie thread consumes some system resources, and when enough zombie threads have accumulated, it will no longer be possible to create new threads (or processes). 

There is no pthreads analog of waitpid(-1, &status, 0), that is, "join with any terminated thread". If you believe you need this functionality, you probably need to rethink your application design. 

All of the threads in a process are peers: any thread can join with any other thread in the process.

在成功调用 pthread_join() 之后,调用方可以确保目标线程已经终止。随后调用方可以选择在线程终止后执行任何必要的清理工作(例如,释放分配给目标线程的内存或其他资源)。 与一个先前已被连接过的线程再次进行连接会导致未定义行为。 未能连接一个可连接的线程(即非detached的线程)会产生”僵尸线程”。应避免这种情况,因为每个僵尸线程都会消耗一定的系统资源,当积累足够多的僵尸线程时,系统将无法再创建新的线程(或进程)pthreads 中没有类似于 waitpid(-1, &status, 0) 的功能,即”与任何已终止的线程连接”。如果你认为需要此功能,可能需要重新考虑应用程序的设计。 进程中的所有线程都是对等的:任何线程都可以连接进程中的任何其他线程。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Index
滚动至顶部