【C++ 面试真题】聊聊 C++ 的线程管理(std::thread)
C++11 之前想写多线程只能抱 pthread 或 Win32 API,跨平台靠宏;C++11 把线程收进了语言标准。背得出"thread 创建线程"只是及格,真考你的是"join 和 detach 不选会怎样、参数怎么传才不会被悬空、jthread 强在哪、和 pthread 比到底赢在哪"。本文是多线程篇的开场,把 <thread> 一次讲透。
一、开场: 家族有哪些?
❓ 介绍一下 C++11 的线程库?
✅ <thread> 一家三口 + 一个 [C++20] 新成员:
| std::thread | 线程本体:创建/join/detach |
| this_thread | 当前线程的工具:get_id/sleep/yield |
| jthread [C++20] | 自动 join 的 thread + 停止令牌 |
| 线程工具 | hardware_concurrency 查核数 |
回答思路:先报"thread 三件套——创建、汇合、分离",再点 this_thread 的睡眠与让步——面试官多半会追"join/detach 不调会怎样"和"参数传递的坑"。
二、创建线程:三种入口
❓ std::thread 怎么创建?能跑什么?
✅ 函数指针、lambda、成员函数,全都行:
// ① 普通函数
void work(int n);
thread t1(work, 10);
// ② lambda(最常用)
thread t2([]{
cout << "hi from lambda";
});
// ③ 成员函数:对象 + &类::方法
struct Task {
void run() {}
};
Task obj;
thread t3(&Task::run, &obj);
构造即启动——没有"start"这一步。参数直接跟在线程对象后面,像转发函数调用一样。
三、join 还是 detach:没有第三条路
❓ 不调 join 也不调 detach 会怎样?
✅ 程序直接 terminate 崩溃——这是 std::thread 的死规矩:线程对象析构时仍 joinable(没 join 没 detach),标准库直接 std::terminate()。
void f() {
thread t(work);
if (err)
return; // ❌ t 析构 → 崩
t.join();
}
两个选择,语义完全不同:
| join | 等它跑完,阻塞当前线程 | 等太久/死等 |
| detach | 放手不管,后台自生自灭 | 引用的东西先死 → 悬空 |
// detach 的经典事故
void f() {
int local = 42;
thread t([&]{ use(local); });
t.detach();
// f 返回、local 消亡
// 后台线程还在用 → UAF
}
⚠️ detach 三思:分离线程的生存期可能超过它引用的一切。要用 detach,线程内部只能持有自己独占的资源(按值捕获、智能指针 move 进去),或用"通知退出"机制管理生命周期。
提前 return 的坑还能用 RAII 收口——给 thread 穿上"自动汇合"的外衣:
class JoinGuard {
thread t_;
public:
explicit JoinGuard(thread t)
: t_(std::move(t)) {}
~JoinGuard() {
if (t_.joinable())
t_.join(); // 任何路径都汇合
}
};
// f() 里:
JoinGuard g{thread{work}};
if (err) return; // ✅ 析构自动 join
这正是 [C++20] jthread 的思路——标准库替你把这个包装类写好了。
四、参数传递:默认拷贝是保护也是坑
❓ 给线程传参数要注意什么?
✅ 参数默认按值拷贝进线程内部存储——这是保护(避免悬空引用),但想传引用必须显式声明:
int x = 0;
// ❌ 这里仍是拷贝!
// thread t(work, x);
// 传引用要 ref:
thread t1(set, ref(x)); // ✅ 真引用
// 成员函数 + 参数混搭:
thread t2(&Task::run, &obj,
arg1, arg2);
⚠️ 最阴的坑:lambda 按引用捕获局部变量 + detach——[&] 抓的是 f 的栈帧,f 一返回全悬空。同上节结论:跨线程生命周期,要么拷贝、要么智能指针转移所有权。
五、this_thread:当前线程的三件工具
❓ this_thread 都有什么?
✅ 四个工具全在当前线程身上:
// ① 我是谁
auto id = this_thread::get_id();
// ② 睡眠:指定时长(C++14 后缀字面量)
this_thread::sleep_for(100ms);
// ③ 睡到指定时间点
this_thread::sleep_until(
steady_clock::now() + 1s);
// ④ 让出时间片(自旋等待时用)
this_thread::yield();
💡 永远别用 C 的 sleep(3):它阻塞的是进程级信号处理的旧模型,且不可移植。this_thread::sleep_for 类型安全、跨平台、可配合各种时钟。yield() 的正确用法是自旋等待里降低 CPU 占用,不是"歇一会儿"的万能调味料。
六、对比 pthread / Win32:标准库赢在哪?
❓ 有了 pthread,为什么还要 std::thread?
✅ 四个维度的差距:
| 跨平台 | 宏地狱两套代码 | 一套代码全平台 |
| 参数 | void* 强转来回 | 类型安全任意参数 |
| RAII | 自己封装 | jthread 自动 join |
| 生态 | 孤立 | mutex/atomic/future 同标准 |
// pthread 时代:传参靠 void* 硬转
void* work(void* arg) {
int n = *(int*)arg; // 谁懂这类型
}
// std::thread:参数直接进
thread t(work, 42);
🎯 一句话总结:pthread 提供的是系统调用包装,std::thread 提供的是语言级抽象——类型安全、异常安全(配合 RAII)、和标准库并发组件无缝拼接。[C++20] 的 jthread 更进一步:析构自动 join、内置 stop_token 协作式取消,把"忘 join 崩溃"和"线程杀不死"两大痛点一起解决。
七、批量线程:vector 管理一小队
❓ 开 10 个线程干完活再继续,怎么组织?
✅ vector<thread> + 循环 join,多线程版本的"fork-join":
vector<thread> pool;
for (int i = 0; i < 10; ++i)
pool.emplace_back(work, i);
// 全部汇合
for (auto& t : pool)
t.join();
要带返回值?别用共享变量硬凑——<future> 的 async/packaged_task 是正路(本篇不展开,追问见下)。
⚠️ 注意 emplace_back 可能抛 bad_alloc/system_error(线程创建失败),严格健壮的代码要接住并 join 已创建的。工程上直接用线程池库(如 boost::asio::thread_pool)更稳。
八、面试高频追问
❓ Q1:thread 对象可以拷贝吗?
✅ 不可以——thread 只能移动(move-only),和 unique_ptr 一个待遇。放进容器要 std::move 或 emplace_back 原地构造。这也保证了"一个线程实体只有一个管理者",避免 double join。
❓ Q2:join 之后再 join 会怎样?
✅ std::terminate 崩溃。join/detach 之后线程对象不再 joinable,再调是错误。多次 join 的防护习惯:join 前判 if (t.joinable())。
❓ Q3:jthread 的 stop_token 怎么用?
✅ 线程函数收 stop_token 参数,循环里查 stoken.stop_requested() 决定退出;外部调 jt.request_stop() 发出停止请求。析构时 jthread 自动 request_stop + join——优雅退出的完整闭环,比 detach 安全、比杀线程文明。
❓ Q4:hardware_concurrency 返回什么?能当线程数用吗?
✅ 返回硬件线程上下文数(核数 × 超线程),是提示不是承诺——可能返回 0(探测失败)。线程池大小参考它,但 IO 密集任务该开更多(等待时 CPU 空闲),纯计算则超过它只是浪费上下文切换。
❓ Q5:线程函数抛了异常会怎样?
✅ 异常出不了线程函数边界——未捕获则 std::terminate 整个进程崩。要跨线程传异常/结果,用 std::promise/future:线程内 set_exception,另一端 future.get() 重新抛出。
❓ Q6:怎么给线程命名(方便调试)?
✅ 标准库没有官方接口——name 是平台扩展(pthread_setname_np / SetThreadDescription)。工程实践:调试期用平台 API 打标,或干脆用日志前缀携带 thread::id。
❓ Q7:线程和进程有什么区别?
✅ 进程是资源分配的单位(独立地址空间、文件表),线程是调度执行的单位(共享所在进程的地址空间和资源,独有栈与寄存器上下文)。推论:线程间通信天然就是读写共享变量(所以才有本篇之后的一切同步话题),进程间通信要走管道/共享内存等内核机制;一个崩了连累整个进程,另一个崩了进程各自安好。
九、总结速查表
| 创建即启动 | 构造 thread 就跑,无 start |
| join/detach | 必二选一,否则析构 terminate |
| detach 风险 | 引用对象先死 → UAF |
| 传参 | 默认拷贝;真引用要 ref |
| sleep | sleep_for + 100ms 字面量 |
| yield | 自旋等待降低占用 |
| thread 可拷贝? | 否,只能移动 |
| jthread [C++20] | 自动 join + stop_token |
| 批量 | vector + 循环 join |
| 跨线程异常 | promise/future 的 set_exception |
一句话回顾
thread 构造即启动、join/detach 必二选一(不选就 terminate);参数默认拷贝保护你,真引用要 ref;detach 放走的线程只能持有独立资源;标准库赢 pthread 的是类型安全 + RAII 生态,[C++20] jthread 自动 join + stop_token 把最后两个痛点补齐。
如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们继续多线程篇——聊互斥锁与读写锁:lock_guard/unique_lock 怎么选、死锁怎么破、读写锁什么时候反而更慢,敬请关注 👋


