跳至内容

std::thread、std::jthread 与线程管理

学习使用 std::thread 和 C++20 的 std::jthread 创建和管理线程。理解 join 与 detach 的区别、使用停止令牌的协作式取消、线程局部存储以及跨线程边界的异常处理。

为什么需要并发?

现代 CPU 拥有多个核心,要充分利用它们,你的程序必须并发地运行工作。C++11 引入了 中的可移植线程库,C++20 又通过 std::jthread 对其进行了改进。线程是一条独立的执行路径,与创建它的线程共享同一地址空间 ── 这既是它的强大之处,也是它的危险所在。

与进程不同,线程共享内存。这使得通信速度很快,但如果两个线程在没有同步的情况下读写同一个变量,也会引入数据竞争。在我们讨论同步(下一课)之前,让我们先学习如何创建、管理和干净地关闭线程。

使用 std::thread 创建线程

你可以通过向 std::thread 传递一个可调用对象(函数、lambda、函数对象)和任意参数来创建线程。线程在构造时立即开始执行。

create_thread.cpp
#include <iostream>
#include <thread>

void greet(const std::string& name, int times) {
    for (int i = 0; i < times; ++i) {
        std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
    }
}

int main() {
    // Launch a thread running greet("Alice", 3)
    std::thread t1(greet, "Alice", 3);

    // Lambda-based thread
    std::thread t2([]() {
        std::cout << "Running in a lambda thread\n";
    });

    // MUST join or detach before t1/t2 are destroyed
    t1.join();  // block until t1 finishes
    t2.join();

    std::cout << "All threads finished\n";
    return 0;
}

join() 与 detach()

每个代表正在运行的线程的 std::thread 在其析构函数运行之前,必须被汇合(join)或分离(detach)。如果你忘记了,std::terminate() 将被调用,程序会崩溃。

join() 阻塞调用线程,直到目标线程完成。这是安全的默认选择 ── 你知道线程已经完成,其结果已经准备好。

detach() 切断 std::thread 对象与操作系统线程之间的关联。线程在后台运行(成为「守护线程」),你将无法再汇合它。要小心:如果分离的线程访问了创建函数中的栈变量,而该函数已经返回,你将得到未定义行为。

常见陷阱

永远不要对捕获或引用了创建作用域中局部变量的线程调用 detach()。当创建函数返回时,那些局部变量会被销毁,而分离的线程将访问悬垂内存。这是未定义行为,通常表现为偶发性崩溃。如果你必须分离,请按值传递数据,或使用 std::shared_ptr 来延长生命周期。

std::jthread ── RAII 线程(C++20)

std::jthread 解决了 std::thread 的两个问题:(1)它在析构函数中自动汇合,所以你永远不会忘记;(2)它通过 std::stop_token 支持协作式取消。

jthread_demo.cpp
#include <iostream>
#include <thread>
#include <chrono>

void worker(std::stop_token stoken, int id) {
    while (!stoken.stop_requested()) {
        std::cout << "Worker " << id << " running...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    std::cout << "Worker " << id << " stopping gracefully\n";
}

int main() {
    std::jthread jt1(worker, 1);  // stop_token is auto-passed as first arg
    std::jthread jt2(worker, 2);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    // Request both threads to stop
    jt1.request_stop();
    jt2.request_stop();

    // No need to call join() — jthread's destructor does it
    std::cout << "Main thread exiting\n";
    return 0;
}

查询硬件与线程局部存储

std::thread::hardware_concurrency() 返回硬件支持的并发线程数。thread_local 存储期为每个线程提供变量的独立副本。

hardware_tls.cpp
#include <iostream>
#include <thread>
#include <vector>

thread_local int tls_counter = 0;  // each thread gets its own copy

void count_work(int id) {
    for (int i = 0; i < 1000; ++i) {
        ++tls_counter;  // no data race — each thread has its own
    }
    std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}

int main() {
    unsigned int n = std::thread::hardware_concurrency();
    std::cout << "Hardware concurrency: " << n << " threads\n";

    std::vector<std::thread> threads;
    for (unsigned int i = 0; i < n; ++i) {
        threads.emplace_back(count_work, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    // Each thread printed 1000 — they don't share tls_counter
    return 0;
}
最佳实践

在 C++20 及以后的版本中,优先使用 std::jthread 而非 std::thread。它在析构函数中自动汇合,消除了忘记汇合而调用 std::terminate 的风险。其内置的 std::stop_token 机制提供了一种干净、标准化的方式来请求协作式取消 ── 无需自己实现 atomic shouldStop 标志。

跨线程异常处理

在线程内部抛出的异常如果未在该线程内被捕获,将调用 std::terminate()。异常不会自动传播到汇合线程。

要在线程之间传递异常,请在工作线程中使用 std::current_exception() 捕获异常,并将其存储在 std::exception_ptr 中。汇合线程随后可以使用 std::rethrow_exception() 重新抛出异常。std::future 机制(在第 4 课中介绍)会自动处理此问题。

关键要点
  • std::thread 立即开始执行;你必须在其析构函数运行之前调用 join() 或 detach()
  • std::jthread(C++20)在其析构函数中自动汇合,并通过 std::stop_token 支持协作式取消
  • thread_local 为每个线程提供变量的独立副本 ── 无需同步
  • std::thread::hardware_concurrency() 告诉你硬件能并发运行多少个线程
  • 线程中的异常不会传播到调用者 ── 使用 std::exception_ptr 或 std::future 来传递它们
  • 永远不要让分离的线程引用创建作用域中的局部变量

测验 — 检验你的知识

(15 经验值)

1. 如果一个代表可汇合线程的 `std::thread` 对象在未调用 `join()` 或 `detach()` 的情况下被销毁,会发生什么?

2. `std::jthread` 如何支持协作式取消?

3. `thread_local` 关键字的作用是什么?