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 就能跑着玩。

相关推荐
xlq223228 小时前
22.多态(上)
开发语言·c++·算法
D_evil__9 小时前
[C++高频精进] 并发编程:线程基础
c++
Mr_WangAndy10 小时前
C++17 新特性_第二章 C++17 语言特性_std::any和string_view
c++·string_view·c++40周年·c++17新特性·c++新特性any
水天需01011 小时前
C++ 三种指针转换深度解析
c++
言言的底层世界12 小时前
c++中STL容器及算法等
开发语言·c++·经验分享·笔记
Mr_WangAndy12 小时前
C++17 新特性_第一章 C++17 语言特性___has_include,u8字符字面量
c++·c++40周年·c++17新特性·__has_include·u8字面量
liu****12 小时前
八.函数递归
c语言·开发语言·数据结构·c++·算法
Vanranrr13 小时前
C++临时对象与悬空指针:一个导致资源加载失败的隐藏陷阱
服务器·c++·算法
BestOrNothing_201513 小时前
【C++基础】Day 5:struct 与 class
c++·c·class类·struct结构体·typename模板·private与public
枫叶丹413 小时前
【Qt开发】Qt窗口(三) -> QStatusBar状态栏
c语言·开发语言·数据库·c++·qt·microsoft