《Rust编程入门》12.1 线程与 std::thread 基础

Rust 的并发编程以安全性和高效性为核心设计目标。通过 std::thread 标准库模块,Rust 提供了对线程的简单而强大的支持。Rust 的线程模型以操作系统线程为基础,结合其所有权和类型系统,确保并发编程中的安全性和可靠性。

12.1 线程与 std::thread 基础

Rust 的并发编程以安全性和高效性为核心设计目标。通过 std::thread 标准库模块,Rust 提供了对线程的简单而强大的支持。Rust 的线程模型以操作系统线程为基础,结合其所有权和类型系统,确保并发编程中的安全性和可靠性。


12.1.1 什么是线程?

线程是计算机程序的最小执行单元。在同一个程序中,多个线程可以并发执行,彼此独立但共享相同的内存空间。Rust 使用操作系统的线程实现多线程并发。

Rust 的线程模型

  • 轻量级:Rust 的线程直接映射到操作系统线程,没有额外的运行时开销。
  • 安全性:通过 Rust 的所有权系统,避免数据竞争和未定义行为。
  • 易用性:提供直观的 API,便于创建和管理线程。

12.1.2 创建线程

Rust 提供了 std::thread::spawn 方法,用于创建新的线程。

基本示例

use std::thread;

fn main() {
    thread::spawn(|| {
        for i in 1..5 {
            println!("Hello from the spawned thread: {}", i);
        }
    });

    for i in 1..5 {
        println!("Hello from the main thread: {}", i);
    }
}

运行结果
主线程和子线程的输出会交替出现,具体顺序由操作系统调度决定。

12.1.3 使用 join 等待线程完成

默认情况下,主线程不会等待子线程完成。可以使用 join 方法来阻塞主线程,直到子线程完成执行。

使用 join 的示例

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("Spawned thread: {}", i);
        }
    });

    handle.join().unwrap(); // 等待子线程完成

    println!("Main thread finished.");
}
  • join 返回 Result,需要使用 unwrap 或模式匹配处理可能的错误。
  • join 确保子线程完成后,主线程才继续执行。

12.1.4 线程中的闭包与数据捕获

子线程可以使用闭包捕获数据,但需要注意所有权问题。Rust 的线程安全设计通过编译期检查,确保数据的所有权是安全的。

示例:移动数据到线程

use std::thread;

fn main() {
    let message = String::from("Hello from Rust!");

    let handle = thread::spawn(move || {
        println!("{}", message);
    });

    handle.join().unwrap();
}
  • 使用 move 关键字,将变量的所有权转移到线程中。
  • 如果没有 move,编译器会报错,因为主线程可能继续使用 message

12.1.5 多线程与共享数据

多个线程可以共享数据,但需要注意数据竞争问题。Rust 的所有权和借用规则禁止多个线程同时访问同一数据,除非使用线程安全的工具。

使用 Arc 共享数据

std::sync::Arc 是一个原子引用计数智能指针,允许多个线程安全地共享数据。

示例:共享数据

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);

    let mut handles = vec![];

    for _ in 0..3 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            println!("{:?}", data);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}
  • Arc::clone 提供安全的共享。
  • 确保数据在所有线程完成前不会被释放。

12.1.6 线程的命名与调试

Rust 提供了命名线程的功能,便于调试。

命名线程

use std::thread;

fn main() {
    let handle = thread::Builder::new()
        .name("WorkerThread".to_string())
        .spawn(|| {
            println!("Hello from the worker thread!");
        })
        .unwrap();

    handle.join().unwrap();
}
  • 使用 thread::Builder 设置线程名。
  • 在调试工具中可以更容易区分线程。

12.1.7 常见错误与注意事项

线程生命周期

子线程的生命周期必须与其捕获的数据兼容。如果数据在主线程结束时被释放,而子线程尚未完成,则会引发错误。

示例:错误的生命周期

use std::thread;

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

    let handle = thread::spawn(move || {
        println!("{:?}", v);
    });

    // 主线程可能先退出,导致 v 被释放。
    // 正确做法是确保 handle.join() 被调用。
}

避免数据竞争

Rust 的编译器会强制确保数据竞争问题被解决。如果需要共享可变状态,必须使用线程安全工具,例如 MutexRwLock

12.1.8 小结

  • Rust 的 std::thread 提供了轻量级、安全的线程支持。
  • 使用 join 可以等待线程完成,确保线程按预期执行。
  • 通过 Arc 等工具,可以安全地在多个线程间共享数据。
  • 结合 Rust 的所有权模型,线程安全在编译阶段即可保证,大幅减少运行时错误。

下一节将介绍 Rust 的消息传递模型和 mpsc(多生产者单消费者)通道,用于实现线程间的通信与协作。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页