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:

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

最后帮你一句话记忆

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

  • 一旦 detach:

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

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

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

相关推荐
沫璃染墨2 分钟前
《从零入门Linux系统篇(五十一):线程篇·四——pthread线程库详解:从线程创建到终止与分离》
linux·运维·服务器·开发语言·c++·系统架构·线程
shada19 分钟前
CMake指定 MSVC 工具集与 Windows SDK
c++·windows
垆边人似月.23 分钟前
华为机考题:矩阵中的最长递增路径
c语言·c++
A7bert7771 小时前
【SAM3部署至AGX Orin】环境配置→模型部署→问题记录
c++·人工智能·深度学习·ubuntu
longlongzihan1 小时前
LeetCode 56合并区间:排序与贪心
c++·算法·leetcode
程序员老陆1 小时前
C++ 继承方式:公有、保护、私有,到底在控制什么
开发语言·c++
坤坤子吖1 小时前
C++11——Lambda、function、bind 与可调用对象
c++·笔记·学习
白杨尚青1 小时前
C++入门篇(十一):string(中)——容量与增删改查:一篇吃透所有常用接口(万字详解)
java·开发语言·c++
朔北之忘 Clancy2 小时前
2024 CSP-J 第二轮真题解析
c++·算法·青少年编程·noip·csp·信奥赛·noi
Lazionr2 小时前
set和map的使用——从基础接口到实际应用
c++