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?

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

相关推荐
律宏阔4 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔4 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴4 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千4 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei374 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei374 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei374 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh4 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW4 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
百万蹄蹄向前冲4 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js