news 2026/9/28 4:40:31

Linux系统篇41——线程(六) 三种退出方式、detach和线程的隔离性

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux系统篇41——线程(六) 三种退出方式、detach和线程的隔离性


📚 本文收录于「流浪」的系列专栏

🐧Linux系统⚙️C++
📊数据结构与算法🐍Python
🔗LangChain & LangGraph🗄️MySQL 数据库
🌿Git 工具🌐计算机网络
🤖AI💯大厂面试、八股
📚学习筑基专栏

🏠 博客主页:流浪 | 📝 原创首发于 CSDN


前言:上一篇拆了 pthread_t 的地址真相、flag 实验钉死了共享。本篇:线程结束时 return、pthread_exit、cancel 三种退出方式,detach 怎么放手,线程之间为什么没有隔离墙。线程控制的全貌就齐了。


一、什么是线程结束

聊退出之前,先把「什么叫做线程结束了」这件事说清楚。main 的结束、入口函数的结束、exit 的调用,各自代表什么,语义不立住,后面三种终止方式就会讲成一锅粥。

1.1 main函数结束,代表什么

一句话三层含义:main 函数结束,代表主线程结束,也代表进程结束。

main 是进程的初始线程,main 里 return 等价于调 exit——POSIX 标准的原文口径:从 main 返回,行为与调用 exit() 并传入返回值完全相同。而 exit 终止的是整个进程:所有线程无论干到哪一步,全部强制结束。

1.2 入口函数结束,代表当前线程结束

新线程所在的入口函数执行结束,就代表当前这条线程运行结束——不动别人,只收自己。

对比着记:主线程(main)结束是全体 termination,子线程入口函数结束只收自己。同是「函数返回」,因为所在线程的地位不同,效果天差地别。

1.3 任何一个线程都不能用exit终止自己

线程想退场,能不能调 exit?不能。因为 exit 终止的是进程——任何一个线程调 exit,整个进程连同所有线程一起被带走,不管别的线程愿不愿意。

所以 exit 在多线程程序里只有一个用途:整个进程确实要收摊了。线程想只结束自己,用下一章的 pthread_exit。

1.4 join只等健康跑完的线程

线程(四)讲的 pthread_join,定位要说准:它解决的是线程健康跑完的情况,不负责处理异常信号。

原因在信号的归属权——异常信号是进程级的处理(第五章细说链路),一个线程被信号带走时,进程本身已经终止了,不存在「join 一个异常退出的线程」这回事,自然也看不到什么结果。所以 join 的世界里只有一种东西:线程正常退出时留下的状态。


二、传参和返回值,可以是任意类型

线程(四)讲过 pthread_create 的第四参数和入口函数的返回 void*——当时说「要传多个值就打包成结构体传地址」。这一章把这句话展开:给线程传递的参数和返回值,可以是任意类型。void* 只是个筐,装什么由你设计。

2.1 参数,打包成

要把一堆数据交给线程,最干净的做法是封装成一个类,把对象地址从第四参数递进去:

Task*t=newTask(10,20);// 任务数据打进对象pthread_create(&tid,nullptr,routine,t);// 对象地址当 arg

入口函数里把 void* 还原成 Task*,数据就全拿回来了。参数是几个数字还是一个上下文,对接口毫无影响——接口不变,类型随便换,这就是 void* 设计的弹性。

2.2 返回值,打包成Result

返回方向同理。线程(四)说过入口函数返回的 void* 就是返回值通道——结果只有一个整数,转成 void* 塞回去就行;结果是一组数据,就封装成 Result 类对象,返回它的地址,join 那边还原。

指向哪有讲究——不能指向线程自己的栈,这是下一章的栈坑,先记住结论:指向堆上 new/malloc 出来的空间,或者全局、静态区。

#include<iostream>#include<pthread.h>classTask{public:Task(inta,intb):_a(a),_b(b){}intExecute(){return_a+_b;}~Task(){}private:int_a;int_b;};classResult{public:Result(intresult):_result(result){}intGetResult(){return_result;}~Result(){}private:int_result;};void*routine(void*tem){Task*t=(Task*)tem;Result*result=newResult(t->Execute());std::cout<<"结果是"<<result->GetResult()<<std::endl;deleteresult;returnnullptr;}intmain(){Task*t=newTask(10,20);pthread_t tid;pthread_create(&tid,nullptr,routine,(void*)t);pthread_join(tid,nullptr);deletet;return0;}


三、线程的退出,三种终止方式

结束的语义立住了,这一章看线程主动退场的三条路。线程(四)讲 join 时留过一个清单——退出状态有三个来源:线程函数 return 的值、pthread_exit 的参数、被 pthread_cancel 时的固定宏。逐个拆开。

3.1 return,最常规的收场

入口函数执行到 return,线程自然结束——1.2 的语义:入口函数结束,当前线程结束,返回值就是退出状态。最常规,没有额外动作。

void*routine(void*tem){Task*t=(Task*)tem;Result*result=newResult(t->Execute());std::cout<<"结果是"<<result->GetResult()<<std::endl;deleteresult;returnnullptr;}

主线程的情况 1.1 已经立住了:main 里 return 等价 exit,全体终止。所以 return 作为退出方式,只适用于子线程。

3.2 pthread_exit,谁调用谁退出

voidpthread_exit(void*retval);// 参数即退出状态,join 能拿到

谁调用谁退出——主线程调它就只结束主线程,其他线程照常跑,进程要等所有线程都跑完才终止。

void*routine(void*tem){Task*t=(Task*)tem;Result*result=newResult(t->Execute());std::cout<<"结果是"<<result->GetResult()<<std::endl;deleteresult;// return nullptr;pthread_exit(result);}intmain(){Task*t=newTask(10,20);pthread_t tid;pthread_create(&tid,nullptr,routine,(void*)t);void*statu=nullptr;pthread_join(tid,&statu);Result*res=(Result*)statu;std::cout<<"退出码为:"<<res->GetResult()<<std::endl;deletet;deleteres;return0;}

子线程里 return 和 pthread_exit 基本等价:子线程 return 到达终点时,库会隐式帮你调一次 pthread_exit,把返回值转成交出状态。差别只在主线程:return 是叫上所有人一起走,pthread_exit 是自己先走。

3.3 retval不能指向线程自己的栈

pthread_exit 的参数是个指针坑,必须记住:retval 指向的对象不能在线程自己的栈上。

线程退出后,它的栈立刻作废,栈上的局部变量随时被复写。join 拿到这个指针时,指向的内容早就不是当年的值——一个野指针。2.2 的结论在这里兑现:要传数据出去,指向全局、静态,或者堆上 new/malloc 出来的空间。

3.4 pthread_cancel,别的线程喊停

前两种都是线程自己退,这一种是别的线程让它退:

intpthread_cancel(pthread_t thread);
void*newidea(void*mes){while(true){std::string name=(char*)mes;std::cout<<"我是新线程,我的name:"<<name<<" "<<"我的pid是:"<<getpid()<<std::endl;sleep(1);}return(void*)123;}intmain(){pthread_t tid;pthread_create(&tid,nullptr,newidea,(void*)"pthread-1");sleep(1);void*val;pthread_cancel(tid);pthread_join(tid,&val);std::cout<<"ret is : "<<(longlongint)val<<std::endl;return0;}

被取消的线程,退出状态固定为 PTHREAD_CANCELED 这个宏——join 拿到它,一眼就知道这个线程是被取消的,不是正常退出的。

这个宏的值有讲究:POSIX 只规定它是一个「不与任何内存对象指针相等、也不等于 NULL」的特殊 (void*) 值;Linux 的 glibc 实现里,它就是-1——所以有些资料会说「被取消的线程退出结果是 -1」。

还有一个容易被忽略的事实:join 是确认取消完成的唯一方式。man 手册的原文——pthread_cancel 返回 0 只说明取消请求成功递出,目标线程是否真的终止了,只有 join 它才知道。

3.5 取消不是立刻生效的

pthread_cancel 发出后,目标线程马上就停吗?不一定。取消的生效时机由目标线程自己的两个属性决定:

  • 取消状态:可取消(默认)或忽略取消——设成忽略,请求就先挂着
  • 取消类型:延迟取消(默认)或异步取消

默认组合是「可取消 + 延迟」:取消请求递到之后并不立即执行,挂起等线程跑到下一个取消点。取消点是一批系统规定的位置——read、write、sleep、pthread_join 这些可能阻塞的调用处,完整清单在 man pthreads(7) 里。线程跑到取消点,检查有没有挂起的取消请求,有就执行。

这带来一个实际问题:一段纯计算的死循环里没有任何取消点——pthread_cancel 发一万次,线程照样转。解法是线程自己在循环里插一个pthread_testcancel(),手动造一个取消点。

取消真正生效时,线程做的事等价于调pthread_exit(PTHREAD_CANCELED):清理函数逆序执行、线程退出,join 拿到那个宏。Linux 上这个请求的递送也和信号系列对得上——NPTL 线程库用第一个实时信号(32 号)把取消请求送进目标线程。

异步取消(PTHREAD_CANCEL_ASYNCHRONOUS)可以做到随到随取消,但随时可能砍在线程持有资源、状态没收拾干净的瞬间——官方文档明确不推荐,知道有这个东西就行。

void*worker(void*arg){(void)arg;volatilelonglongcnt=0;while(1){cnt++;// 实验 A:什么都不加。纯计算循环不调用任何函数,// 没有任何取消点,cancel 请求只能一直挂着。// (刻意不 printf:它要往 stdout 写,某些实现里// 也可能被做成取消点,会污染实验)// 实验 B:把这行注释打开,循环里有了手动取消点// pthread_testcancel();}returnNULL;}intmain(){pthread_t tid;pthread_create(&tid,NULL,worker,NULL);sleep(1);// 给新线程一点跑起来的时间pthread_cancel(tid);// 只是发一个取消"请求",不保证停void*ret=NULL;pthread_join(tid,&ret);// 等线程真的停下来if(ret==PTHREAD_CANCELED)printf("线程被取消,join 拿到 PTHREAD_CANCELED\n");elseprintf("join 正常返回,线程不是被取消的\n");return0;}

四、pthread_detach,分离线程

4.1 为什么要分离

join 是手动回收:等线程结束、取退出状态、释放资源,三步一次不落。要结果的线程,这一趟值得。

但有些线程天生是后台干活的——日志、心跳、定时清理,主线程根本不关心它返回什么;服务器高并发场景每个请求起一个线程,主线程也不可能挨个 join 一遍。对这些线程,join 是纯负担。

pthread_detach 就是给这个场景的:

intpthread_detach(pthread_t thread);

把线程设为分离状态:它一退出,库自动回收它的全部资源,不需要任何人 join。

有一个容易误解的点要掰正:分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程依旧可以访问、可以操作——分离改变的是资源回收方式,不是把线程踢出进程。

4.2 joinable和分离互斥

规矩很硬:一个线程要么被 join,要么被分离,二选一。

  • 对分离的线程调 pthread_join,直接报错 EINVAL——它已经不是 joinable 状态了
  • 分离是单向门:一旦分离,不能被 join,也不能改回 joinable
  • 对已经分离的线程再 detach 一次,是未定义行为

反方向的账线程(四)算过:joinable 的线程不 join 不 detach,就是僵尸线程,TCB 和退出状态永久挂着,泄漏照算。所以每个线程出生时都带着一道选择题——join 还是 detach,必须选一个。

还有一条 man 手册明确提醒的边界:分离不改变线程的从属关系——进程 exit(等价于主线程 return)时,分离线程照样被强制终止,不会因为「分离开了」就多活一秒。想让后台线程跑完,主线程要么 join 要么 pthread_exit,不能直接 return 走人。

void*newidea(void*mes){pthread_detach(pthread_self());std::string name=(char*)mes;std::cout<<"我是新线程,我的name:"<<name<<" "<<"我的pid是:"<<getpid()<<std::endl;sleep(1);return(void*)123;}intmain(){pthread_t tid;pthread_create(&tid,nullptr,newidea,(void*)"pthread-1");sleep(2);void*val;pthread_join(tid,&val);std::cout<<"ret is : "<<(longlongint)val<<std::endl;return0;}

4.3 两种分离姿势

分离有两个方向,对应两种姿势:

1. 主线程分离新线程

创建之后主线程调 pthread_detach(tid),从此不管。

2. 新线程自己分离自己

最常见的写法是新线程入口第一行:

void*thread_func(void*arg){pthread_detach(pthread_self());// man 手册原生的标准示例// ... 干活 ...returnNULL;}

从此自生自灭,主线程完全不用管。

课堂 demo(代码由浪哥填充):新线程分离后,主线程尝试对它调 pthread_join——试试会出什么结果:观察返回 EINVAL 报错,印证 4.2 的单向门。


五、隔离性,线程之间没有隔离墙

5.1 一个线程崩,全进程崩

线程(三)给线程列缺点时说过「健壮性低——一个线程崩全进程崩」,当时按住了没展开,这里正面拆。

一个线程野指针、数组越界、除 0——CPU 触发硬件异常,内核捕获后把异常转成信号(SIGSEGV、SIGFPE 这类),信号发给的是整个进程。信号的处理粒度本来就是进程级(线程(四)kill -9 的账),默认处理是终止进程——于是所有线程一起陪葬。除 0 的例子线程(三)写过,这里不重演。

void*newidea(void*mes){inta=10/0;return(void*)123;}intmain(){pthread_t tid;pthread_create(&tid,nullptr,newidea,(void*)"pthread-1");void*val;pthread_join(tid,&val);return0;}

为什么操作系统这么狠?因为线程共享地址空间,一个线程把共享数据踩坏了,别的线程读到的全是脏的——无法确认谁还干净,只能整体终止。这不是设计缺陷,是共享的必然代价。1.4 说 join 不管异常信号,根子也在这:信号一到,进程都没了,join 没有可以等待的对象。

5.2 进程之间有墙,线程之间没有

把进程和线程放一起看,「隔离」这个词才立体:

对比项进程之间同进程线程之间
地址空间各自独立共享同一个
隔离墙有——各自的页表把空间隔开没有,一锅端
崩溃波及崩了不牵连别的进程一个崩,全进程陪葬
数据往来要走进程间通信直接读写共享变量
拿什么换稳定,但通信贵通信零成本,但无隔离

进程之间的那堵墙,就是页表——线程(二)拆过:每个进程一套页表,CR3 一换,谁也看不见谁的空间,这就是隔离的物理载体。同进程的线程共享同一套页表,天然没有墙——flag 实验里子线程改完全局变量主线程立刻可见,是共享的福利;一个线程崩了全员陪葬,是共享的账单。一张页表,两头都写明白了。

5.3 健壮性低和复用结构更健壮,为什么不矛盾

上一篇 2.3 已经正面回答过这对看似打架的结论(它串起篇36「复用结构更健壮」和线程(三)「健壮性低」两个说法),这里不再重讲,只接一句定性:内核实现层的健壮和应用层的脆弱是两个层面——隔离性的缺失不是内核的锅,是共享的代价,两句话合起来正好是线程的完整画像。


六、全篇总结

一条线收拢:

  • 结束语义:main 结束 = 主线程结束 = 进程结束;入口函数结束 = 当前线程结束;任何线程调 exit 都是终止进程,join 只等健康跑完的线程、不管异常信号
  • 传参返回值:void* 是筐,参数打包成 Task、返回值打包成 Result,接口不变、类型随便换;指向别碰线程自己的栈
  • 三种退出:return 最常规但只适用于子线程(主线程等价 exit 全体陪葬);pthread_exit 谁调用谁退出;pthread_cancel 退出状态固定 PTHREAD_CANCELED(glibc 里就是 -1);默认延迟取消,要跑到取消点才生效,纯计算循环得 pthread_testcancel 手动造点
  • detach:分离换自动回收,与 join 互斥、单向门、不能改回;主线程分离新线程、新线程自己分离自己两种姿势;进程 exit 面前分离线程照样终止
  • 隔离:线程之间没有墙——页表是进程的墙,线程共享页表所以一崩全崩;健壮性两层说法上一篇已经对齐

七、文末面试题

7.1 推导题(按本讲知识点,附答案)

先自己想,再看答案——答案都用本章的逻辑推,不引入新知识。

  1. 【推导】主线程 return 和 pthread_exit 有什么区别?
    答:main 里 return 等价于调 exit,整个进程终止,所有线程强制结束;主线程调 pthread_exit 只结束自己,其他线程照常运行,进程等所有线程跑完才终止。

  2. 【推导】pthread_exit 的返回值为什么不能指向栈上局部变量?
    答:线程退出后自己的栈立刻作废,栈内存随时被复写,join 拿到的是野指针。要传数据就指向全局、静态或堆上的对象。

  3. 【推导】被 pthread_cancel 取消的线程,join 拿到什么?
    答:固定是 PTHREAD_CANCELED 宏(glibc 实现里值为 -1),一眼区分被取消和正常退出。pthread_cancel 返回 0 只说明请求递出了,join 是确认取消完成的唯一方式。

  4. 【推导】detach 之后还能 join 吗?还能改回 joinable 吗?
    答:都不能。join 分离线程直接报 EINVAL;分离是单向门,不能改回 joinable;对已分离线程再 detach 是未定义行为。join 或 detach 必须二选一。

  5. 【推导】为什么取消线程不是立刻生效的?
    答:默认是延迟取消——请求挂起,等线程跑到下一个取消点(read/write/sleep 这类可能阻塞的调用处)才执行。纯计算循环没有取消点,要靠 pthread_testcancel 手动造一个。

  6. 【推导】分离的线程,主线程 return 后还会继续跑吗?
    答:不会。detach 只改变资源回收方式,不改变从属关系——进程 exit 时分离线程照样被强制终止。

  7. 【推导】join 能等到一个被信号杀死的线程吗?
    答:不能,这个问题本身不成立。异常信号是进程级处理,线程被信号带走时进程已经终止,不存在可供 join 的退出状态——join 的世界里只有健康退出的线程。

7.2 真题(来源已核实,转述注明)

  1. 主线程退出,其他线程会退出吗?
    答:看主线程怎么退。return 或调 exit——整个进程终止,其他线程全部强制结束;调 pthread_exit——只结束主线程自己,其他线程继续运行,进程等所有线程结束才终止。
    【真题·转述自 CSDN 博客《6 Linux系统编程-面试题》(blog.csdn.net/bdarkray/article/details/153815650)

  2. 为什么一个线程崩溃会导致整个进程崩溃?
    答:线程共享进程地址空间,野指针、越界、除 0 触发硬件异常,内核转成信号发给整个进程(信号的处理粒度是进程级),默认动作终止进程,所有线程陪葬;且共享数据可能已被破坏、无法确认影响范围,只能整体终止。
    【真题·转述自 博客园《操作系统面试题总结》(www.cnblogs.com/WindSun/p/13562923.html)


💬 结束的语义立住了,Task/Result 打包传参收结果,return、pthread_exit、pthread_cancel 三条退出路走通,detach 换自动回收,没墙的隔离一崩全崩——线程的控制接口到此收官。觉得有收获,点个赞再走。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 4:40:28

15年做哪个网站能致富新手入门指南

15年做哪个网站能致富新手入门指南 备案流程一头雾水,是不是让你对着后台页面发呆,连第一步该点哪里都搞不清楚?很多新手入门时,最大的焦虑不是技术,而是合规。别慌,今天咱们就掰开揉碎了讲,避开那些让你钱包失血的坑,看看15年做哪个网站能致富。 方案类型与适用场景…

作者头像 李华
网站建设 2026/9/28 4:40:08

第1讲:Jev 概念与基础接入

一、Jev 是什么Jev 是 TypeSafe AI 于 2026 年 9 月发布的决策模型&#xff0c;定位为 AI 应用的「系统1」——快速、低成本、确定性的判断引擎。与传统 LLM 的核心区别维度传统 LLMJev输出形式自然语言文本结构化决策结果&#xff08;选项/分数/概率&#xff09;延迟1-10秒70-…

作者头像 李华
网站建设 2026/9/28 4:39:39

django肥胖儿童运动与健康管理系统58555-计算机课程设计、毕业设计

前言 ✨ 博主介绍&#xff1a;一线全栈工程师&#xff0c;毕设实战引路人。技术栈覆盖Java、Python、C#、PHP、Node.js及UniApp跨端开发&#xff0c;擅长多语言项目落地与架构设计。持续分享毕设源码、开题报告、技术选型心得与职场踩坑经验。用工程化思维写代码&#xff0c;帮…

作者头像 李华
网站建设 2026/9/28 4:39:26

wordpress里放代码避坑指南:5个关键注意事项

wordpress里放代码避坑指南:5个关键注意事项 域名解析指向错误,服务器配置没理顺,代码一丢上去网站直接打不开?这种惨剧在WordPress站点里太常见了。很多中小企业老板觉得,既然装了WordPress,那不就是个博客吗?怎么搞个代码嵌入还要掉坑里?这里面的 注意事项…

作者头像 李华
网站建设 2026/9/28 4:39:07

3步解决wordpress输密码访问难题,一文搞懂流量密码

3步解决wordpress输密码访问难题,一文搞懂流量密码 网站做好了没人访问,这是很多站长深夜崩溃的真实写照。你精心设计的页面、辛苦撰写的文章,在搜索引擎眼里却像隐形人。别急着投广告,先检查一下你的WordPress后台配置。很多时候,流量断崖式下跌的根源,恰恰藏在那个不起眼的“wordpress…

作者头像 李华
网站建设 2026/9/28 4:38:49

建筑网站开发设计新手入门:3类方案报价拆解,别再花冤枉钱

建筑网站开发设计新手入门:3类方案报价拆解,别再花冤枉钱 网站做好了没人访问,这是90%的建筑行业新手建站者遇到的第一道坎。你花了大几千甚至上万块,找外包做了一个看起来“挺高大上”的建筑网站,结果上线三个月,百度后台显示日均访客不足5人。为什么?因为你在建筑网站开发设计的起步阶段,选错了方案,或者被…

作者头像 李华