std::thread、std::jthread 与线程管理
学习使用 std::thread 和 C++20 的 std::jthread 创建和管理线程。理解 join 与 detach 的区别、使用停止令牌的协作式取消、线程局部存储以及跨线程边界的异常处理。
为什么需要并发?
现代 CPU 拥有多个核心,要充分利用它们,你的程序必须并发地运行工作。C++11 引入了 中的可移植线程库,C++20 又通过 std::jthread 对其进行了改进。线程是一条独立的执行路径,与创建它的线程共享同一地址空间 ── 这既是它的强大之处,也是它的危险所在。
与进程不同,线程共享内存。这使得通信速度很快,但如果两个线程在没有同步的情况下读写同一个变量,也会引入数据竞争。在我们讨论同步(下一课)之前,让我们先学习如何创建、管理和干净地关闭线程。
使用 std::thread 创建线程
你可以通过向 std::thread 传递一个可调用对象(函数、lambda、函数对象)和任意参数来创建线程。线程在构造时立即开始执行。
#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 支持协作式取消。
#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 存储期为每个线程提供变量的独立副本。
#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 标志。
跨线程异常处理
在线程内部抛出的异常如果未在该线程内被捕获,将调用 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` 关键字的作用是什么?