目录
[1. 危险示例:detach 后访问已经析构的对象(典型踩坑)](#1. 危险示例:detach 后访问已经析构的对象(典型踩坑))
[2. 安全示例:detach 后只用自己的数据](#2. 安全示例:detach 后只用自己的数据)
[2.1 全部按值拷贝(最简单安全)](#2.1 全部按值拷贝(最简单安全))
[2.2 用 std::shared_ptr 管理共享对象](#2.2 用 std::shared_ptr 管理共享对象)
[3. 线程比"调用它的函数"活得久,但不能比程序久](#3. 线程比“调用它的函数”活得久,但不能比程序久)
[4. 必须用 join 的场景:需要结果 / 同步](#4. 必须用 join 的场景:需要结果 / 同步)
[4.1 需要线程返回的结果](#4.1 需要线程返回的结果)
[4.2 需要"等他干完再继续后面的逻辑"](#4.2 需要“等他干完再继续后面的逻辑”)
直接上几个小而典型的场景代码 ,你可以自己改着跑一跑感受一下。
我分成 4 类:
-
危险的
detach:用到了已经析构的对象 -
安全的
detach:全部用自己的数据(拷贝 / 智能指针) -
子线程比创建它的函数"活得更久",但不能比整个程序久
-
明显必须用
join的场景(需要结果 / 同步)
1. 危险示例:detach 后访问已经析构的对象(典型踩坑)
#include <iostream>
#include <thread>
#include <chrono>
void bad_worker(int& ref) { // 注意这里是引用
std::this_thread::sleep_for(std::chrono::seconds(1));
// 这里很可能已经在访问"悬空引用"
std::cout << "bad_worker ref = " << ref << std::endl;
}
void bad_detach_example() {
int x = 42;
std::thread t(bad_worker, std::ref(x));
t.detach(); // 把线程放飞
// 函数马上结束,x 被析构
std::cout << "bad_detach_example end, x is destroyed\n";
}
int main() {
bad_detach_example();
std::this_thread::sleep_for(std::chrono::seconds(2));
}
问题点:
-
线程函数
bad_worker里拿的是int& ref,指向bad_detach_example的局部变量x。 -
detach之后,bad_detach_example很快结束,x被销毁。 -
1 秒后线程还在用
ref(等于在用一个已经不存在的变量)→ 未定义行为(有时崩,有时输出乱值,有时"看上去正常")。
把局部变量引用/指针丢给一个
detach线程,是最典型的炸弹。
2. 安全示例:detach 后只用自己的数据
2.1 全部按值拷贝(最简单安全)
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
void safe_worker(std::string msg) { // 按值接收,内部有自己的拷贝
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "safe_worker: " << msg << std::endl;
}
void safe_detach_example_value_copy() {
std::string s = "hello, detach";
// 这里会把 s 拷贝到新线程栈里
std::thread t(safe_worker, s);
t.detach(); // 放飞没问题
std::cout << "safe_detach_example_value_copy end\n";
}
int main() {
safe_detach_example_value_copy();
std::this_thread::sleep_for(std::chrono::seconds(2));
}
这里:
-
即使
safe_detach_example_value_copy结束,s析构了也没事; -
线程里用的是
msg这个自己的参数拷贝。
2.2 用 std::shared_ptr 管理共享对象
#include <iostream>
#include <thread>
#include <memory>
#include <chrono>
struct Data {
int value;
};
void safe_worker_shared(std::shared_ptr<Data> p) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "safe_worker_shared value = " << p->value << std::endl;
}
void safe_detach_example_shared_ptr() {
auto data = std::make_shared<Data>();
data->value = 123;
// 线程拿到的是 shared_ptr 的一份拷贝(引用计数 +1)
std::thread t(safe_worker_shared, data);
t.detach(); // 放飞没问题
// 这里即使 data 离开作用域,引用计数 -1,但线程还有一份
std::cout << "safe_detach_example_shared_ptr end\n";
}
int main() {
safe_detach_example_shared_ptr();
std::this_thread::sleep_for(std::chrono::seconds(2));
}
这里:
-
主线程和子线程都持有
shared_ptr<Data>; -
只要有任意一方还在用,对象就不会被释放;
-
线程结束后释放自己那份,最终计数变 0,内存回收。
3. 线程比"调用它的函数"活得久,但不能比程序久
模拟一个函数里启动了线程,线程干比较久的活,但函数返回得很快:
#include <iostream>
#include <thread>
#include <chrono>
void long_task() {
for (int i = 0; i < 5; ++i) {
std::cout << "long_task step " << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "long_task done\n";
}
void start_background_task() {
std::thread t(long_task);
t.detach(); // 放飞,让它在后台慢慢干
std::cout << "start_background_task end\n";
} // 函数结束,这里没关系
int main() {
start_background_task();
std::cout << "main is doing something else...\n";
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "main is about to exit\n";
}
运行效果大概是:
-
start_background_task很快就结束; -
long_task还在继续输出step 0,1,2,...; -
main睡 3 秒后退出 → 程序结束,后台线程也被 OS 一起干掉(可能来不及打印完所有 step)。
要点:
-
这里就体现了:线程可以比创建它的函数(
start_background_task)活得久。 -
但程序(
main)一结束,整个进程就没了,线程肯定也没了,不可能"程序都没了它还在跑"。
4. 必须用 join 的场景:需要结果 / 同步
4.1 需要线程返回的结果
#include <iostream>
#include <thread>
void calc_sum(int a, int b, int& out) {
out = a + b;
}
int main() {
int result = 0;
std::thread t(calc_sum, 1, 2, std::ref(result));
// 必须等待线程算完再用 result
t.join(); // 如果这里用 detach,就会有数据竞争 / 未定义行为
std::cout << "result = " << result << std::endl;
}
这里:
-
如果改成
t.detach(),主线程根本不知道什么时候result被写好; -
可能还没算完你就打印了,甚至程序先结束了,线程还在写 → 各种乱七八糟问题;
-
这就是典型必须 join 的场景。
4.2 需要"等他干完再继续后面的逻辑"
#include <iostream>
#include <thread>
#include <chrono>
void init_subsystem() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "subsystem initialized\n";
}
int main() {
std::thread t(init_subsystem);
// 这里要保证初始化完成,才能往下走
t.join(); // 必须等初始化线程结束
std::cout << "now we can safely use the subsystem\n";
}
如果这里用 detach:
- 主线程不等初始化就往下跑,开始"使用还没初始化好的东西",就会乱套。
最后帮你一句话记忆
-
能
join就join,除非你真的不在乎结果,只是丢个后台任务(日志、监控之类)。 -
一旦
detach:-
子线程用到的数据要么是自己的拷贝,要么是智能指针等安全托管;
-
绝不能拿局部变量的引用/裸指针去用。
-
如果你想,我可以再帮你把这些代码整理成一个完整的小 demo 工程结构(几个 .cpp 分场景),你直接复制进 IDE 就能跑着玩。