ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

Linux系统篇41——线程(六) 三种退出方式、detach和线程的隔离性 本文收录于「流浪」的系列专栏Linux系统⚙️C数据结构与算法PythonLangChain 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*tnewTask(10,20);// 任务数据打进对象pthread_create(tid,nullptr,routine,t);// 对象地址当 arg入口函数里把 void* 还原成 Task*数据就全拿回来了。参数是几个数字还是一个上下文对接口毫无影响——接口不变类型随便换这就是 void* 设计的弹性。2.2 返回值打包成Result返回方向同理。线程四说过入口函数返回的 void* 就是返回值通道——结果只有一个整数转成 void* 塞回去就行结果是一组数据就封装成 Result 类对象返回它的地址join 那边还原。指向哪有讲究——不能指向线程自己的栈这是下一章的栈坑先记住结论指向堆上 new/malloc 出来的空间或者全局、静态区。#includeiostream#includepthread.hclassTask{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*resultnewResult(t-Execute());std::cout结果是result-GetResult()std::endl;deleteresult;returnnullptr;}intmain(){Task*tnewTask(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*resultnewResult(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*resultnewResult(t-Execute());std::cout结果是result-GetResult()std::endl;deleteresult;// return nullptr;pthread_exit(result);}intmain(){Task*tnewTask(10,20);pthread_t tid;pthread_create(tid,nullptr,routine,(void*)t);void*statunullptr;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我是新线程我的namename 我的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::coutret is : (longlongint)valstd::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;volatilelonglongcnt0;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*retNULL;pthread_join(tid,ret);// 等线程真的停下来if(retPTHREAD_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我是新线程我的namename 我的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::coutret is : (longlongint)valstd::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){inta10/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_CANCELEDglibc 里就是 -1默认延迟取消要跑到取消点才生效纯计算循环得 pthread_testcancel 手动造点detach分离换自动回收与 join 互斥、单向门、不能改回主线程分离新线程、新线程自己分离自己两种姿势进程 exit 面前分离线程照样终止隔离线程之间没有墙——页表是进程的墙线程共享页表所以一崩全崩健壮性两层说法上一篇已经对齐七、文末面试题7.1 推导题(按本讲知识点,附答案)先自己想再看答案——答案都用本章的逻辑推不引入新知识。【推导】主线程 return 和 pthread_exit 有什么区别答:main 里 return 等价于调 exit整个进程终止所有线程强制结束主线程调 pthread_exit 只结束自己其他线程照常运行进程等所有线程跑完才终止。【推导】pthread_exit 的返回值为什么不能指向栈上局部变量答:线程退出后自己的栈立刻作废栈内存随时被复写join 拿到的是野指针。要传数据就指向全局、静态或堆上的对象。【推导】被 pthread_cancel 取消的线程join 拿到什么答:固定是 PTHREAD_CANCELED 宏glibc 实现里值为 -1一眼区分被取消和正常退出。pthread_cancel 返回 0 只说明请求递出了join 是确认取消完成的唯一方式。【推导】detach 之后还能 join 吗还能改回 joinable 吗答:都不能。join 分离线程直接报 EINVAL分离是单向门不能改回 joinable对已分离线程再 detach 是未定义行为。join 或 detach 必须二选一。【推导】为什么取消线程不是立刻生效的答:默认是延迟取消——请求挂起等线程跑到下一个取消点read/write/sleep 这类可能阻塞的调用处才执行。纯计算循环没有取消点要靠 pthread_testcancel 手动造一个。【推导】分离的线程主线程 return 后还会继续跑吗答:不会。detach 只改变资源回收方式不改变从属关系——进程 exit 时分离线程照样被强制终止。【推导】join 能等到一个被信号杀死的线程吗答:不能这个问题本身不成立。异常信号是进程级处理线程被信号带走时进程已经终止不存在可供 join 的退出状态——join 的世界里只有健康退出的线程。7.2 真题(来源已核实,转述注明)主线程退出其他线程会退出吗答:看主线程怎么退。return 或调 exit——整个进程终止其他线程全部强制结束调 pthread_exit——只结束主线程自己其他线程继续运行进程等所有线程结束才终止。【真题·转述自 CSDN 博客《6 Linux系统编程-面试题》(blog.csdn.net/bdarkray/article/details/153815650)为什么一个线程崩溃会导致整个进程崩溃答:线程共享进程地址空间野指针、越界、除 0 触发硬件异常内核转成信号发给整个进程信号的处理粒度是进程级默认动作终止进程所有线程陪葬且共享数据可能已被破坏、无法确认影响范围只能整体终止。【真题·转述自 博客园《操作系统面试题总结》(www.cnblogs.com/WindSun/p/13562923.html) 结束的语义立住了Task/Result 打包传参收结果return、pthread_exit、pthread_cancel 三条退出路走通detach 换自动回收没墙的隔离一崩全崩——线程的控制接口到此收官。觉得有收获点个赞再走。
返回列表