Linux线程控制——从线程创建到线程分离(第三部分:线程等待 pthread_join)

Linux线程控制------从线程创建到线程分离(第三部分:线程等待 pthread_join)

上一章我们讲解了线程的终止方式,线程可通过以下两种形式正常结束运行:

cpp 复制代码
return

cpp 复制代码
pthread_exit()

线程终止后,很多人会产生一个疑问:线程结束以后,是不是所有资源都立即释放了?

答案是否定的。

线程结束仅代表线程停止了代码执行,其退出状态、返回值、线程控制信息等资源并不会自动释放,需要手动处理回收。为了解决线程状态监听与资源回收问题,Linux线程库引入了线程等待机制 pthread_join()

一、为什么需要线程等待?

我们先梳理基础线程执行流程:通过 pthread_create() 创建子线程后,子线程会独立执行任务、计算数据,最终通过返回值结束运行。但主线程无法自主感知子线程的运行状态,存在三个核心疑问:

  • 子线程是否正常执行完成?

  • 线程任务是否执行成功?

  • 线程的执行结果是什么?

如果没有线程等待机制,会出现严重的线程执行失效问题:主线程创建子线程后,会继续向下执行,若主线程率先退出,整个进程会直接终止,进程内所有子线程都会被强制销毁,子线程的任务无法正常执行完成。

问题复现代码

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

// 线程执行函数
void* ThreadRun(void* args)
{
    // 模拟线程耗时任务
    sleep(5);
    cout<<"thread finish"<<endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    // 创建子线程
    pthread_create(&tid, nullptr, ThreadRun, nullptr);

    // 主线程直接退出
    cout<<"main exit"<<endl;
    return 0;
}

运行结果

终端仅输出:

无法输出预期的 thread finish

问题原因

并非子线程没有执行,而是主线程创建子线程后立即退出,导致整个进程终止,操作系统直接回收进程内所有资源,正在休眠的子线程被强制终止,没有机会执行后续代码。

二、pthread_join 的核心作用

pthread_join() 是线程等待的核心函数,主要完成两大核心任务,彻底解决上述线程失效、资源泄露问题。

1. 阻塞等待线程结束

主线程调用pthread_join() 后会进入阻塞状态,暂停自身后续代码执行,直至指定的子线程完全终止后,才会解除阻塞、继续执行后续逻辑。

执行流程:主线程调用 pthread_join() → 阻塞等待 → 子线程执行完毕退出 → 主线程恢复运行。

2. 回收线程残留资源

线程终止后,内核会释放线程的运行资源,但 pthread 用户态线程库会保留线程的退出状态、返回值、线程控制信息等数据,这些资源不会自动释放,会造成内存残留。

调用 pthread_join() 可以主动读取并清空这些残留数据,完成线程资源的完整回收。其作用与进程中的 wait() 函数高度相似。

三、pthread_join 函数原型

复制代码

四、参数详细解析

参数一:thread(线程ID)

类型为 pthread_t,用于指定需要等待回收的目标线程 ,必须是通过 pthread_create() 创建的有效线程ID。

示例:

cpp 复制代码
pthread_t tid;
// 创建线程,获取线程ID
pthread_create(&tid, nullptr, ThreadRun, nullptr);
// 等待指定tid的线程结束
pthread_join(tid, nullptr);

参数二:retval(线程返回值)

类型为二级指针 void**,核心作用是接收线程终止时的返回数据

原理:线程执行函数的返回值为void* 类型(一级指针),主线程需要定义一个一级指针变量接收该返回值,因此需要传入该指针的地址(二级指针)。

若不需要获取线程返回值,直接传入nullptr 即可。

五、返回值说明

  • 执行成功 :返回0

  • 执行失败:返回对应错误码

错误判断示例:

cpp 复制代码
int ret = pthread_join(tid, nullptr);
if(ret != 0)
{
    cout<<"线程等待失败"<<endl;
}

六、pthread_join 基础使用示例

我们在原有代码基础上加入线程等待逻辑,解决主线程提前退出的问题。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

void* ThreadRun(void* args)
{
    // 循环执行耗时任务
    for(int i=0;i<5;i++)
    {
        cout<<"thread running "<<i<<endl;
        sleep(1);
    }
    cout<<"thread finish"<<endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, nullptr);

    // 阻塞等待子线程执行完毕
    pthread_join(tid, nullptr);

    // 子线程结束后,主线程才会执行后续代码
    cout<<"main continue"<<endl;
    return 0;
}

运行结果

复制代码

完整执行流程

主线程创建子线程 → 调用 pthread_join() 进入阻塞 → 子线程循环执行任务 → 子线程执行完毕return退出 → pthread_join() 解除阻塞 → 主线程继续执行后续代码。

七、pthread_join 与进程 wait() 对比

线程等待与进程等待的设计思想高度一致,都是用于等待子执行单元结束、回收资源,核心对比如下:

进程机制 线程机制
fork() 创建子进程 pthread_create() 创建子线程
wait() 等待、回收子进程 pthread_join() 等待、回收子线程
获取子进程退出状态 获取线程执行返回值

核心区别:进程拥有独立的地址空间,而同一进程下的所有线程共享地址空间

八、线程结束后的资源深层解析

很多初学者容易混淆:线程执行终止 ≠ 线程资源完全释放

线程调用 return 或 pthread_exit() 后,仅停止代码执行,内核会释放线程的运行栈、CPU调度资源,但 pthread 用户态库会保留线程的退出状态、返回数据等信息,这些资源会一直残留,直至调用 pthread_join() 完成回收。

该逻辑与僵尸进程类似:子进程退出后,父进程未调用 wait() 回收资源,子进程会变成僵尸进程,保留退出状态。

线程特殊点:Linux 内核实际调度的是 LWP(轻量级进程),线程终止后,内核层 LWP 资源会立即释放,但用户态 pthread 库的线程信息会残留,不会形成"僵尸线程",仅会造成轻微内存泄露。

九、pthread_join 获取线程返回值

线程执行函数返回值为 void* 万能指针,支持返回任意类型的数据(整数、结构体、对象等),适配各类业务场景。

1. 返回整数类型数据

通过堆空间存储整数,避免栈空间失效问题,主线程通过 retval 参数接收并解析数据。

cpp 复制代码
void* ThreadRun(void* args)
{
    // 堆空间创建数据,生命周期不受线程栈影响
    int* result = new int;
    *result = 100;
    return result;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, nullptr);

    void* ret;
    // 接收线程返回的指针
    pthread_join(tid, &ret);

    // 强制类型转换,解析数据
    int* value = (int*)ret;
    cout<<*value<<endl;

    // 手动释放堆内存
    delete value;
    return 0;
}

运行结果:100

2. 返回结构体(项目常用)

实际开发中,单值返回无法满足复杂业务需求,通常使用结构体封装多组结果数据。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

// 定义结果结构体,封装多组数据
struct Result
{
    int sum;    // 总和
    int max;    // 最大值
};

void* ThreadRun(void* args)
{
    // 堆空间创建结构体对象
    Result* result = new Result;
    result->sum = 500;
    result->max = 100;

    sleep(2); // 模拟耗时计算
    return result;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, nullptr);

    void* ret;
    pthread_join(tid, &ret);

    // 解析结构体数据
    Result* result = (Result*)ret;
    cout<<"sum = "<<result->sum<<endl;
    cout<<"max = "<<result->max<<endl;

    // 释放堆内存,避免内存泄露
    delete result;
    return 0;
}

运行结果:

复制代码

十、返回值必须使用堆空间的原因

严禁返回线程栈空间的局部变量地址,属于典型的野指针未定义行为。

错误示例(栈空间返回)

cpp 复制代码
void* ThreadRun(void* args)
{
    // 局部变量,存储在线程栈空间
    Result result;
    result.sum = 100;
    // 返回栈空间地址
    return &result;
}

错误原理

局部变量 result 存储在线程私有栈空间,线程执行结束后,线程栈会被系统完全释放,局部变量数据彻底失效。此时主线程获取的地址为野指针,访问数据会出现乱码、程序崩溃等未知问题。

正确方案

使用 new 在堆空间创建返回对象,堆空间生命周期由开发者手动控制,线程终止后数据不会失效,主线程读取数据后,手动 delete 释放内存,安全无泄露。

十一、pthread_join 核心注意事项

1. 一个线程仅能被 join 一次

首次调用 pthread_join() 会完成线程资源的全部回收,线程信息已被清空,二次调用会直接执行失败。

cpp 复制代码
void* ThreadRun(void* args)
{
    // 局部变量,存储在线程栈空间
    Result result;
    result.sum = 100;
    // 返回栈空间地址
    return &result;
}

2. join 是阻塞式函数

调用后主线程必然阻塞,直至目标线程结束。若线程执行耗时10秒,主线程会阻塞10秒,无法执行后续代码。

3. 仅可等待 joinable 状态的线程

pthread_create() 创建的线程默认处于joinable(可等待) 状态,支持 pthread_join 回收。

后续学习的 pthread_detach() 会将线程转为 detached(分离态),分离后的线程无法再执行 join 操作。

十二、本章总结

核心流程

线程执行任务 → 线程终止 → 系统保留线程退出信息与返回值 → pthread_join() 等待线程结束、读取返回值、回收残留资源 → 主线程恢复执行。

重点知识点

  • pthread_join() 核心作用:阻塞等待线程结束、回收线程用户态资源,等价于进程的 wait()

  • 第二个参数 retval 为万能指针,可接收任意类型的线程返回值。

  • 线程返回值禁止使用栈局部变量地址,必须使用堆空间存储,避免野指针问题。

  • 开发中常用结构体/类对象封装复杂的线程返回结果。

  • 线程仅可 join 一次,分离态线程不支持 join 操作。

下一章预告:线程分离 pthread_detach

后续将深度解析线程分离机制,重点解答以下问题:

  • 为什么部分业务场景不需要使用 pthread_join?

  • 线程分离后,资源由谁自动回收?

  • detach 与 join 的核心区别与适用场景?

  • 为什么分离后的线程无法再次 join?

  • 主线程退出,分离线程为何会同步终止?

相关推荐
无忧.芙桃1 小时前
数据结构之堆
c语言·数据结构·c++·算法·
AAA@峥1 小时前
Ceph RBD 块存储全解析:原理与实操汇总
运维·分布式·ceph
Sirius Wu1 小时前
OpenClaw Model Provider(模型提供商)完整详解
服务器·网络·人工智能·安全·aigc
LONGZETECH2 小时前
新能源汽车动力系统仿真硬核拆解:五系统分层架构的设计逻辑与技术权衡
c语言·开发语言·架构·汽车·汽车仿真教学软件·汽车教学软件
维核科技2 小时前
AI Agent从“玩具“进化成“劳动力“:2026年智能体商业化元年
运维·人工智能·服务器维修·gpu维修
醉城夜风~2 小时前
C++模板详解:从基础到高级全面解析
java·开发语言·c++
谜之锋2 小时前
Linux 源码编译安装 net-snmp-5.9.1 并配置使用 SNMPv3
linux·运维·服务器·snmpv3
RuiZN2 小时前
Muduo---Channel类
运维·服务器·c++
一直在努力学习的菜鸟2 小时前
阿里云2G2核的服务器可以做什么?
linux·运维