线程基础
线程是操作系统调度的基本单位。一个进程可以包含多个线程,它们共享地址空间,各自拥有独立的栈和寄存器状态。
共享地址空间是线程轻量的原因,也是并发 bug 的根源——所有全局变量、堆对象都是共享的。
一、std::thread
std::thread 在构造时立即启动线程,不需要单独调用 start():
#include <iostream>
#include <thread>
void hello(int id)
{
std::cout << "线程 " << id << " 运行中\n";
}
int main()
{
std::thread t1(hello, 1); // 启动线程,参数按值传递
std::thread t2([] { hello(2); }); // 用 lambda
t1.join(); // 等待 t1 结束
t2.join(); // 等待 t2 结束
}
参数默认是拷贝到线程内部的。需要传引用时必须用 std::ref:
void modify(std::vector<int>& v) { v.push_back(1); }
std::vector<int> data;
std::thread t(modify, std::ref(data)); // 不加 ref 会编译失败
t.join();
不加 std::ref 会编译失败其实是好事——它提醒你引用传递有生命周期风险。如果线程比被引用对象活得久,就会访问已销毁的对象。
1. 必须 join 或 detach
std::thread 析构时如果仍处于 joinable 状态,程序会直接调用 std::terminate:
{
std::thread t(hello, 1);
} // 错误:t 析构时未 join/detach,程序终止
这个设计是刻意的:标准委员会认为「线程还在跑但句柄没了」是严重错误,与其静默泄漏不如直接崩掉。
std::thread t(hello, 1);
t.join(); // 等待结束
// 或
t.detach(); // 放手不管,线程在后台运行
detach() 要慎用——分离后的线程无法再被等待或控制,程序退出时它可能还在访问已销毁的全局对象。
2. 常用接口
| 接口 | 含义 |
|---|---|
join() | 阻塞等待线程结束 |
detach() | 让线程独立运行,不再与 thread 对象关联 |
joinable() | 是否关联着一个线程 |
get_id() | 返回线程 ID |
hardware_concurrency() | 硬件支持的并发线程数(静态函数) |
hardware_concurrency() 常用于决定线程池大小:
unsigned n = std::thread::hardware_concurrency();
if (n == 0) n = 4; // 无法确定时的兜底值
它返回的只是「建议值」,可能受 CPU 亲和性、容器配额等因素影响。
3. 异常与线程
线程函数中抛出的异常不会传播到创建它的线程。如果异常逃出线程函数,会直接调用 std::terminate:
std::thread t([] {
throw std::runtime_error("出错了");
// 异常逃出 → std::terminate
});
t.join();
需要把异常传回主线程,得用 std::promise 或 std::exception_ptr:
std::promise<void> p;
std::thread t([&p] {
try {
doWork();
p.set_value();
} catch (...) {
p.set_exception(std::current_exception());
}
});
try {
p.get_future().get(); // 异常在这里重新抛出
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
t.join();
二、std::jthread(C++20)
std::jthread 解决了 thread 的两个痛点:析构时自动 join,以及内置协作式取消。
#include <thread>
int main()
{
std::jthread t([](std::stop_token st) {
while (!st.stop_requested())
{
// 执行任务
}
});
} // 离开作用域时自动 request_stop() 并 join
1. 自动 join
jthread 析构时会先请求停止,再 join。因此不会出现「忘记 join 导致 terminate」的问题:
{
std::jthread t(doWork);
} // 自动停止并等待
2. 协作式取消
jthread 内部持有一个 std::stop_source,构造时把对应的 std::stop_token 作为第一个参数传给线程函数:
void worker(std::stop_token st, int id)
{
while (!st.stop_requested())
{
// 干活
}
std::cout << "线程 " << id << " 收到停止请求\n";
}
std::jthread t(worker, 1);
// 主线程可以主动请求停止
t.request_stop();
| 成员函数 | 作用 |
|---|---|
request_stop() | 请求停止,返回是否成功 |
get_stop_token() | 获取停止令牌 |
get_stop_source() | 获取停止源 |
注意「协作式」的含义:request_stop() 只是设置一个标志,线程必须主动检查 stop_requested() 才能响应。如果线程卡在阻塞调用里,停止请求不会生效。
3. stop_callback
可以在停止请求发出时执行回调:
std::stop_token st = t.get_stop_token();
std::stop_callback cb(st, [] {
std::cout << "收到停止请求\n";
});
这在需要唤醒阻塞操作时很有用——比如让 condition_variable 停止等待。C++20 的 condition_variable_any 提供了支持 stop_token 的 wait 重载:
std::condition_variable_any cv;
std::mutex mtx;
void worker(std::stop_token st)
{
std::unique_lock lock(mtx);
// 收到停止请求时自动唤醒
cv.wait(lock, st, [] { return /* 条件 */; });
}
三、当前线程操作
std::this_thread 命名空间提供了一组操作当前线程的函数:
#include <thread>
#include <chrono>
std::this_thread::get_id(); // 当前线程 ID
std::this_thread::sleep_for(std::chrono::seconds(1)); // 睡眠 1 秒
std::this_thread::sleep_until(tp); // 睡眠到指定时间点
std::this_thread::yield(); // 让出 CPU
1. sleep_for 与 sleep_until
// 相对时间
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 绝对时间
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
std::this_thread::sleep_until(deadline);
在循环中做定时任务时,sleep_until 比 sleep_for 更准——它不会累积每次循环的执行时间误差:
// 不好:误差会累积
while (running) {
doWork();
std::this_thread::sleep_for(100ms);
}
// 好:固定周期
auto next = std::chrono::steady_clock::now();
while (running) {
doWork();
next += 100ms;
std::this_thread::sleep_until(next);
}
2. yield
yield() 建议调度器切换到其它线程,但不保证真的切换:
while (!flag.load()) {
std::this_thread::yield(); // 自旋等待时让出 CPU
}
在自旋锁或忙等待中加 yield() 可以降低 CPU 占用,但不如用条件变量或原子等待高效。
四、线程局部存储
thread_local 声明的变量每个线程各有一份独立副本:
thread_local int counter = 0; // 每个线程独立
void increment()
{
++counter; // 不构成数据竞争
}
这是避免共享状态的利器。典型用途:
随机数引擎(引擎不是线程安全的):
void worker()
{
thread_local std::mt19937 gen(std::random_device{}());
// 每个线程独立的引擎
}
线程专属的缓存:
thread_local std::vector<char> buffer; // 复用缓冲区,避免反复分配
错误状态(类似 errno):
thread_local int last_error = 0;
几点说明:
thread_local变量的初始化在首次使用时发生,每个线程各初始化一次- 线程结束时销毁,析构顺序与构造顺序相反
- 有性能开销:访问
thread_local需要查 TLS 表,比普通全局变量慢 thread_local与static可以组合,表示「每个线程的静态变量」
五、线程数量与开销
线程不是免费的:
| 开销项 | 说明 |
|---|---|
| 栈空间 | 每个线程默认 1~8 MB(Linux 通常 8 MB) |
| 创建成本 | 涉及系统调用,通常几十微秒 |
| 上下文切换 | 每次切换几微秒,频繁切换会显著拖慢程序 |
| 缓存失效 | 切换后 CPU 缓存可能失效 |
因此:
- 不要为每个任务创建一个线程。任务数量大时用线程池。
- 线程数通常设在 CPU 核心数附近。计算密集型任务,线程数超过核心数没有收益。
- I/O 密集型任务可以多设一些。线程阻塞在 I/O 上时不占 CPU。
unsigned n = std::thread::hardware_concurrency();
std::vector<std::jthread> pool;
for (unsigned i = 0; i < n; ++i)
pool.emplace_back(worker, i);
六、常见误区
「std::thread 会自动 join」 —— 不会。析构时如果还 joinable,程序直接终止。用 jthread 可以避免。
「detach() 之后就安全了」 —— 分离的线程可能访问已销毁的对象。除非线程完全自包含,否则不要 detach。
「线程函数抛异常会传播到主线程」 —— 不会,会直接 terminate。需要手动传递。
「sleep_for(0) 等于 yield()」 —— 不完全等价。sleep_for(0) 可能触发系统调用,yield() 只是建议调度。
「hardware_concurrency() 返回的就是最佳线程数」 —— 它只是硬件支持的并发数,实际最优值取决于任务类型(计算密集还是 I/O 密集)。
「thread_local 没有开销」 —— 有。访问 TLS 变量比普通变量慢,热路径上要谨慎。
「线程越多越快」 —— 超过核心数后,上下文切换和缓存失效会拖慢程序。