std::thread¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++11 | 2/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-11-03 |
编译器支持:GCC 4.4+ · Clang 3.0+ · MSVC 16.0+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 std::thread 在构造函数里启动事件循环,并在析构函数里 join | ev-stdexec-thread-single-thread-context-001 |
| 用 std::thread 运行阻塞的 waitpid 循环,避免阻塞启动线程 | ev-hyprland-thread-waitpid-002 |
| 用 std::thread::hardware_concurrency 限制数据库事件循环的线程数 | ev-drogon-thread-hardware-concurrency-003 |
本站证据页:ev-drogon-thread-hardware-concurrency-003 · ev-hyprland-thread-waitpid-002 · ev-stdexec-thread-single-thread-context-001
解决什么问题¶
C++11 之前,标准库没有线程抽象,跨平台代码只能在 pthread、Win32 线程和各自的条件编译之间切换。线程入口必须是固定签名的函数指针,参数通过 void* 传递,错误处理依赖返回值;线程句柄的创建、等待和销毁也没有统一的 RAII 语义。
std::thread 把“启动一个可调用对象作为新线程”变成标准库操作:入口可以是函数、lambda 或带状态的函数对象,参数按值转发给可调用对象,join() 等待结束,detach() 放弃所有权。标准提案为 N2497,头文件是 <thread>。
基础用法¶
构造 std::thread 时传入可调用对象和参数;对象构造完成即代表新线程已经开始执行。线程对象是移动-only 的,只能拥有一个执行体。析构时如果仍处于 joinable 状态,程序会调用 std::terminate,因此每条路径都必须 join() 或 detach()。
#include <functional>
#include <iostream>
#include <thread>
#include <vector>
void accumulate(int from, int to, long long& out) {
long long sum = 0;
for (int value = from; value <= to; ++value) {
sum += value;
}
out = sum;
}
int main() {
const int workers = 4;
const int per_worker = 25;
std::vector<long long> partial(workers, 0);
std::vector<std::thread> threads;
threads.reserve(workers);
for (int i = 0; i < workers; ++i) {
const int from = i * per_worker + 1;
const int to = from + per_worker - 1;
threads.emplace_back(accumulate, from, to, std::ref(partial[i]));
}
std::cout << threads.size() << ' ' << (threads.front().joinable() ? 1 : 0)
<< '\n';
for (auto& worker : threads) {
worker.join();
}
long long total = 0;
for (long long value : partial) {
total += value;
}
std::cout << partial.front() << ' ' << partial.back() << ' ' << total << '\n';
long long doubled = 0;
std::thread lambda_worker([&doubled] { doubled = 21 * 2; });
lambda_worker.join();
std::cout << doubled << '\n';
}
输出 4 1、325 2200 5050 与 42。四个线程分别累加 1–25、26–50、51–75、76–100,主线程在 join() 后读取各自结果再汇总;std::ref 让线程拿到 partial[i] 的引用而不是副本。第二个线程演示 lambda 直接捕获局部变量。完整版本见 examples/cpp11/thread.cpp。
参数默认按值复制到线程的存储中;需要共享对象时用 std::ref/std::cref 显式包装。join() 只能调用一次,调用后线程变为 non-joinable;detach() 让线程在后台继续运行,线程对象不再能等待它。
进阶用法¶
线程入口的可调用对象在构造时就被复制或移动,因此捕获的 unique_ptr 等移动-only 对象要配合 std::move。参数也是按值转发:传引用必须用 std::ref,否则修改的是线程内部的副本。
线程数量应当受控。std::thread::hardware_concurrency() 返回实现建议的并发线程数,但允许返回 0;它只是提示,不是可用核数的保证。数据库连接池、线程池常用它与任务数取较小值来定容。
thread_local 变量为每个线程保存独立实例,适合缓存和错误状态;但它的构造与析构时机由线程生命周期决定,长时间驻留的线程要避免无限增长的线程局部容器。
需要“可中断、可自动 join”的线程时,C++20 的 std::jthread 更合适:析构时自动请求停止并 join,还带 stop_token。只需要一次性异步结果时,std::async 配合 std::future 能省去手工管理线程对象。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| stdexec | include/exec/single_thread_context.hpp#L27-L40 |
构造函数里用 lambda 启动 run_loop,析构函数里 join() |
把线程生命周期绑定到拥有者,构造即启动、析构即汇合 |
| Hyprland | start/src/core/Instance.cpp#L76-L87 |
用独立线程执行阻塞的 waitpid 循环 |
阻塞系统调用移出主线程,避免卡住启动流程 |
| drogon | orm_lib/src/DbClientImpl.cc#L66-L71 |
用 hardware_concurrency() 给数据库事件循环线程数封顶 |
线程池定容时把硬件并发度作为上限 |
三条证据均为 manual 级:作者复核过出处与行号,尚未引入第二名审核者。三处都在生产路径,覆盖线程封装、阻塞调用卸载与线程池定容三种典型形态。
陷阱与边界¶
析构时仍 joinable 会终止进程。 std::thread 的析构函数不会隐式 join,也不会隐式 detach;忘记 join() 或 detach() 会调用 std::terminate。
不能 join 自己。 在目标线程内部对自己的 std::thread 对象调用 join() 会抛 std::system_error,常见于把线程对象放进共享容器后误用。
detach 后的生命周期完全由调用方负责。 分离线程可能访问已经销毁的局部变量或成员;除非线程真正独立,否则优先 join。
共享数据仍需自己同步。 std::thread 只负责创建执行流,不提供数据保护。多个线程同时写同一对象、同时扩容同一容器,都是数据竞争。
线程函数抛出的异常会终止进程。 异常不能穿过线程边界传播;需要在线程内部捕获并转成结果、错误码或 std::exception_ptr。
hardware_concurrency() 可能返回 0。 标准允许实现无法检测时返回 0,直接拿它做除数或容器大小会出问题。
线程不是免费的。 每个线程有独立的栈和内核调度开销;大量短任务应该交给线程池,而不是为每个任务新建线程。
库联动¶
std::jthread:C++20 的自动 join 线程,析构时请求停止并等待,替代大多数裸std::thread成员。std::mutex/std::condition_variable:线程之间传递数据的基本同步原语;std::thread不替你做互斥。std::atomic:无锁计数器和标志位;比互斥量更适合简单共享状态。std::async/std::future:一次性异步任务,省去手工 join 和结果传递。- 线程池与执行器:stdexec、TBB 等提供任务调度;
std::thread更多作为底层执行单元。
横向对比¶
| 方案 | 标准版本 | 生命周期管理 | 返回值传递 | 主要代价 |
|---|---|---|---|---|
std::thread |
C++11 | 手动 join/detach | 需共享变量或包装 | 忘记汇合会终止进程 |
std::jthread |
C++20 | 析构自动 join | 仍需共享变量 | 需要支持 stop_token 的协作式取消 |
std::async |
C++11 | future 管理 | std::future |
启动策略由实现决定 |
pthread |
平台 | 手动 join/detach | void* 参数 |
不可移植,缺少类型安全 |
| 线程池 | 第三方 | 池统一管理 | 任务返回值 | 引入依赖,调优空间与实现绑定 |
跨标准演进:C++11 通过 N2497 引入 std::thread 与整套线程同步设施;C++20 增加 std::jthread 和停止令牌,把协作式取消与自动汇合纳入标准;截至 C++23,线程创建本身仍不提供跨平台的优先级或亲和性接口。