C++线程中detach和join的注意点

目录

[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 类:

  1. 危险的 detach:用到了已经析构的对象

  2. 安全的 detach:全部用自己的数据(拷贝 / 智能指针)

  3. 子线程比创建它的函数"活得更久",但不能比整个程序久

  4. 明显必须用 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

  • 主线程不等初始化就往下跑,开始"使用还没初始化好的东西",就会乱套。

最后帮你一句话记忆

  • joinjoin,除非你真的不在乎结果,只是丢个后台任务(日志、监控之类)

  • 一旦 detach

    • 子线程用到的数据要么是自己的拷贝,要么是智能指针等安全托管;

    • 绝不能拿局部变量的引用/裸指针去用。

如果你想,我可以再帮你把这些代码整理成一个完整的小 demo 工程结构(几个 .cpp 分场景),你直接复制进 IDE 就能跑着玩。

相关推荐
一个不知名程序员www23 分钟前
算法学习入门---C/C++输入输出
c语言·c++
qq_433554541 小时前
C++ 状压DP(01矩阵约束问题)
c++·算法·矩阵
千里马-horse1 小时前
CallbackInfo
c++·node.js·napi·callbackinfo
何小义的AI进阶路1 小时前
win下 vscode下 C++和opencv的配置与使用
c++·图像处理·vscode·opencv
XXYBMOOO2 小时前
理解 C++ 中的字节序转换函数 `swapEndian`
开发语言·arm开发·c++
毕加锁2 小时前
深度解析昇腾Catlass:C++模板元编程与高性能算子开发范式(1)
开发语言·c++
你好音视频2 小时前
FFmpeg FLV编码器原理深度解析
c++·ffmpeg·音视频
Qt学视觉3 小时前
PaddlePaddle-2wget下载安装
c++·人工智能·paddlepaddle
老秦包你会3 小时前
C++进阶------C++的类型转换
java·开发语言·c++