当前位置: 首页 > news >正文

【Linux】线程深度指南:从等待、分离到 C++ 多线程实战,一文搞懂线程 ID 与进程空间(4)


一、线程等待

#include <iostream> #include <pthread.h> #include <cstdio> #include <stdlib.h> #include <ctime> #include <unistd.h> #include "Task.hpp" #include <vector> void* Routine(void* args) { std::string name = static_cast<const char*>(args); while(true) { std::cout << "new thread" << name << std::endl; sleep(5); break; } return (void*)10; } //线程等待 int main() { pthread_t tid; pthread_create(&tid,nullptr,Routine,(void*)"thread_1"); //等待线程退出,否则会出现类似于进程僵尸问题 void* retval = nullptr; int n = pthread_join(tid,&retval); if(n == 0)//等待成功 { std::cout << "join success: " << (long long)retval << std::endl; } return 0; }

注意:使用 ps -aL 是查不到线程退出时的僵尸状态的,因为 ps -aL 查的是系统级的轻量级进程,在内核中已经把这个线程释放了,但是线程管理的相关消息在 pthread 库内部中还存在。

问题:pthread_join 获取到的退出消息,为什么没有退出信号?为什么没有进行异常分析

答:新线程出异常,进程全部退出,线程没机会 join 成功,所以线程不需要关系异常。

注意:Routine 的返回值不一定是整数,他可以是一个字符串、类对象等。

#include <iostream> #include <pthread.h> #include <cstdio> #include <stdlib.h> #include <ctime> #include <unistd.h> #include "Task.hpp" #include <vector> void* Routine(void* args) { std::string name = static_cast<const char*>(args); while(true) { std::cout << "new thread" << name << std::endl; sleep(1); } } //线程等待 int main() { pthread_t tid; pthread_create(&tid,nullptr,Routine,(void*)"thread_1"); //等待线程退出,否则会出现类似于进程僵尸问题 sleep(5); pthread_cancel(tid);//主动杀掉指定线程 void* retval = nullptr; int n = pthread_join(tid,&retval); if(n == 0)//等待成功 { std::cout << "join success: " << (long long)retval << std::endl; } return 0; }

如果是主线程主动杀掉线程时,线程的退出消息为 -1(PTHREAD_CANCELED);

二、分离线程

默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

如果不关心线程的返回值,join 是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。此时把线程设置成分离状态就行。

#include <iostream> #include <pthread.h> #include <cstdio> #include <stdlib.h> #include <ctime> #include <unistd.h> #include "Task.hpp" #include <vector> void* Routine(void* args) { std::string name = static_cast<const char*>(args); int cnt = 5; while(cnt--) { std::cout << "new thread" << name << std::endl; sleep(1); } return (void*)10; } //线程等待 int main() { pthread_t tid; pthread_create(&tid,nullptr,Routine,(void*)"thread_1"); sleep(2); pthread_detach(tid);//把线程设置分离状态 void* retval = nullptr; int n = pthread_join(tid,&retval); if(n == 0)//等待成功 { std::cout << "join success: " << (long long)retval << std::endl; } else { sstd::cout << "join erroe: " << n << std::endl; } return 0; }

如果把线程设置成分离状态,主线程还要等待线程的话,必然会等待失败。

注意:一旦线程被设置成分离状态,则主线程不能提前退,甚至主线程是个死循环。既然把线程分离了,但是一旦线程出了问题,例如:除0,野指针等,还是会导致整个进程崩掉。

三、线程 ID 和进程地址空间布局

首先 pthread 库也是库,要加载到内存,进而映射到当前进程的虚拟地址空间,以便支持线程控制,在之前的我们可以知道线程的 id 值是一个非常大的数字,现在我可以明确的是线程的 id 值就是一个地址!线程既然可以有多个,那么有的线程正在运行、正在退出、正在分离等,这么多的线程肯定是要被管理起来的,怎么管理?先描述,再组织;注意:这里的线程概念在 Linux 内核中是没有体现出来的,内核里只有轻量级进程的概念,所以线程概念只有在 pthread 库里面体现!所以描述线程,使用 stuct Tcb 来描述,不是在 Linux 内核中描述,而是在 pthread 库里面描述,而组织请看下图:

我们每创建一个线程,在虚拟地址空间里面的 pthread 库就会创建该线程的属性信息,这些属性信息包括:struct pthread 结构体(这个结构体就是上面说的 Tcb)、线程局部存储、线程栈。而线程 id 值或者说 tid 就是这个属性信息的起始地址!!因为每个线程的属性信息在虚拟地址空间上是连续的,所以我们可以联想成是一个数组来管理这些线程的属性信息。在 struct pthread 结构体里面有个变量 void * result 这个变量来存储线程退出的信息,也就是说当 Routine 函数的返回值起始就是赋值给这个变量 result ,而 pthread_join 获取到线程退出的信息也就是把这个变量 result 赋值给我们的传过去的变量。

我们创建线程的底层是调用 clone 系统调用,而 clone 函数是用来创建轻量级进程或者进程的,在 clone 中如果不用创建虚拟地址空间、页表等数据结构,那么就是要创建轻量级进程,即:只创建 PCB ,那么我们可以得出一个结论:每创建一个线程都会在系统层面上有对应的 PCB ,也就是说:struct pthread : struct task_struct = 1 :1,这个称为:1:1 式的用户及线程;而 PCB 承担线程的调度、保存轻量级进程的上下文数据等作用,而 struct pthread 结构体是在库里面描述线程的属性,只是 PCB 承担了线程的大部分属性,所以 struct pthread 里面的属性才会这么少。所以线程退出时,PCB 是直接释放的,所以我们使用 pa -aL 来查线程是查不到的,但是 struct pthread 还存在,所以我们要等待线程释放掉这个 struct pthread 结构体以及该线程的线程局部存储、线程栈。struct pthread 和 线程局部存储、线程栈我们叫做线程的描述信息。

线程局部性存储:

#include <iostream> #include <pthread.h> #include <cstdio> #include <stdlib.h> #include <ctime> #include <unistd.h> #include "Task.hpp" #include <vector> //此时还没有就行线程局部存储 pid_t id = 0; void* Routine(void* args) { std::string name = static_cast<const char*>(args); while (true) { std::cout << "new thread id: " << id << std::endl; id++; sleep(1); } return nullptr; } int main() { pthread_t tid; pthread_create(&tid,nullptr,Routine,(void*)"thread_1"); while (true) { std::cout << "main thread id : " << id << std::endl; sleep(1); } pthread_join(tid,nullptr); return 0; }

上面的出现这张情况的原因:因为线程局部性存储是给每个线程开辟一段栈空间,也就是上面这个 id 值的地址在每个线程的中的虚拟地址是不一样的:

注意:mmap 区域其实就是虚拟地址空间的共享区;上面图片中的线程栈是专门给线程使用的,而上上张图里面的主线程栈是给主线程使用的。

线程局部性存储:只能用来局部性存储内置类型,常见的是整型;

线程局部性存储的意义:可以让不同的线程用同样的变量名,访问不同的内存块,各自访问各自的局部存储。

注意:线程局部性存储这个工作/过程是编译器做的。

四、C++ 的多线程(代码示范)

#include <iostream> #include <unistd.h> #include <thread> void Routine(int cnt) { while (cnt--) { std::cout << "new thread : " << cnt << std::endl; sleep(1); cnt--; } } int main() { std::thread t(Routine,10);//创建线程 while(true) { std::cout << "main thread" << std::endl; sleep(1); } t.join();//线程等待 return 0; }
TestThread:TestThread.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f TestThread

使用C++来编写线程,在 Linux 中能跑,而且把这份代码复制到 Windows 平台下也能跑,这证明 C++ 具有很强的跨平台性(把所有的平台对应的线程代码用 C++ 封装,对外提供统一的接口),C++ 多线程的本质是在Linux系统中 C++ 多线程操作对 pthread 库的封装。

问题:为什么 C++ 性特性支持要以年为单位?

答:从技术角度,他要把所有的平台对应的功能全部封装一遍。

问题:为什么所有语言都追求跨平台性?

答:Windows 和 Linux 背后的用户的百万级别的,如果 C++ 只支持其中的一方,就会导致 C++ 失去了其中一个平台的用户,总之支持跨平台性就是为了让更多用户使用。


未完待续!

http://www.cnnetsun.cn/news/387907.html

相关文章:

  • 万字硬核评测:Gemini 3 Pro与GPT-5.2谁才是版本之子?附企业级聚合接入实战指南
  • Sonic数字人可用于抖音/B站/快手内容创作
  • ssm基于SSM的钢铁工厂生产管理系统的设计与实现_ljn28073
  • 侧脸照片能用Sonic吗?建议正面或轻微角度
  • 个人创作者能免费使用Sonic吗?非商用完全自由
  • Sonic数字人参加线上发布会?代替真人主持
  • C#调用Sonic DLL库?Windows平台集成方案
  • Sonic数字人模型适配多种格式:MP3、WAV通吃
  • 导师推荐!9款一键生成论文工具测评:本科生毕业论文写作全攻略
  • LUT调色包下载后如何美化Sonic输出视频?后期流程
  • 为什么Sonic成为数字人落地的关键推手?
  • 动漫风格图片能用吗?写实优先,卡通需测试
  • Sonic数字人已规模化落地,背后的技术驱动力是什么?
  • 社区论坛运营:鼓励用户之间交流Sonic使用经验
  • QQ浏览器搜索优化:提升Sonic关键词排名
  • uniapp+Springboot面向移动端的房屋租赁系统 小程序
  • 钉钉宜搭集成:让企业用户在办公系统内使用Sonic
  • 安全扫描实施:定期扫描Sonic代码库是否存在漏洞
  • 自动伸缩策略:根据Sonic请求量动态调整计算资源
  • 定制化开发报价:为大客户提供私有化部署Sonic服务
  • 让嘴形更贴合节奏:dynamic_scale在Sonic中的作用解析
  • Token计费模式设计:为Sonic API调用制定合理定价
  • 昆仑芯适配进展:百度自研芯片运行Sonic的实测
  • 融资路演材料:向投资人讲述Sonic的巨大市场潜力
  • 冷启动解决方案:最初如何吸引第一批Sonic种子用户
  • OBS虚拟摄像头终极指南:从零开始的多场景应用完全教程
  • YouTube演示视频脚本:向全球用户介绍Sonic功能
  • 定价页面设计:清晰呈现Sonic不同套餐的性价比
  • B站UP主合作计划:邀请创作者体验Sonic数字人魅力
  • 99%开发者忽略的External Memory漏洞(Java 17+权限管理全曝光)