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?
-
主线程退出,分离线程为何会同步终止?