Linux:线程控制和原生线程库

文章目录

  • 线程的id和LWP
  • 线程的终止
  • 线程的返回值问题
  • 关于原生线程库问题

本篇总结的内容主要是关于线程的控制专题

线程的id和LWP

对于获取线程的id来说,在Linux系统中存在这样的调用

在这里插入图片描述

这个调用就可以获取返回当前线程的id

先写出下面的实例代码

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;// 新线程
void *ThreadRoutine(void *arg)
{const char *threadname = (const char *)arg;while (true){cout << threadname << " thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");while (true){cout << "main thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;sleep(1);}return 0;
}

在这里插入图片描述

从这个代码中可以看出一个现象,那就是获取的线程的id和LWP是不一样的,这也就意味着LWP和id是两个不同的内容,如果将这个数字转换成十六进制,会发现这个数字其实很像一个地址,而事实上这个东西确确实实是一个地址,关于这个地址的概念在后续会进行阐述

线程的终止

那么下面要进行讨论的内容是,线程的终止问题,对于一个线程来说,它如何进行终止?由于线程在Linux中可以看成是一个轻量级的进程,那么这里先回顾一下对于进程的终止概念,对于进程来说想要终止可以进程退出,或是通过return进行退出,那么在这里也可以类比过来,线程也可以进行适当的退出操作,但是在之前的内容中知道,线程是不可以直接进行exit的,线程直接进行exit会导致所有的线程都进行退出,原因就在于exit表示的是进程退出,而不是线程退出,所以任何一个线程调用这个exit函数都会导致整个所有的线程都退出,因为它们共同属于一个进程

那么线程想要单独退出也是有对应的退出接口的,其中一个退出接口就叫做pthread_exit调用

在这里插入图片描述

那么这个接口的作用就是直接对应的线程进行终止掉,所以现在想要终止一个线程,不仅可以调用return语句进行调用,也可以使用对应的调用接口进行退出

线程的返回值问题

下面来到的话题是关于线程的返回值问题,那对于线程的返回值问题来说,首先要搞清楚的是返回值的意义是什么,那这个问题其实很好明白,就是为了让外部的主线程能够获取到对应的返回值,进而进行对应的操作,那下面的问题是如何获取?如果没有及时获取是否会出现类似于进程的僵尸问题?

获取线程的返回值

获取线程的返回值,在Linux系统中存在一个系统调用:

在这里插入图片描述
这个系统调用的作用之一,就是可以获取到指定线程的退出信息,第二个参数是一个二级指针,表明这是一个输出型参数,它想要获取的内容是一个void*类型的参数,而这个参数实际上就是对应在创建线程的时候调用时会返回的值,这个函数可以回收指定线程id的线程,并且把返回值的信息存储到第二个这个输出型参数当中,这样就能完成对于线程的返回值获取

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;class pthread_exit_info
{
public:pthread_exit_info(int code, const string &info): _code(code), _info(info){}public:int _code;string _info;
};// 新线程
void *ThreadRoutine(void *arg)
{const char *threadname = (const char *)arg;cout << threadname << " thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;cout << threadname << " thread exit" << endl;sleep(2);return (void *)new pthread_exit_info(0, "exit normal");
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");cout << "main thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;sleep(5);void* res = nullptr;pthread_join(tid, &res);pthread_exit_info* exit_info = (pthread_exit_info*)res;cout << "线程的退出码是: " << exit_info->_code << " 线程的退出信息是: " << exit_info->_info << endl;return 0;
}

在这里插入图片描述

线程的僵尸问题

对于进程来说,如果子进程结束了,但是父进程一直没有对子进程进行资源的回收,那么子进程就会进入僵尸状态,子进程的PCB等资源还在内存中进行占用,需要父进程调用对应的接口进行回收,那对于线程来说当然也会存在这样的问题,但是这个现象不是很明显的观察,所以这里只是会输出这样的观点,确实在操作系统内部会存在线程的僵尸状态的问题,但没有一个明确的规定标准

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;// 新线程
void *ThreadRoutine(void *arg)
{const char *threadname = (const char *)arg;cout << threadname << " thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;cout << threadname << " thread exit" << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");cout << "main thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;sleep(5);pthread_join(tid, nullptr);return 0;
}

在这里插入图片描述
从上述的现象图中确实可以看到,是存在一个这样的退出的动态过程的,而这个join的过程也是一个阻塞等待的过程,所以在观察现象的时候可以看到,是存在这样的一个从2个线程创建出来到1个线程到没有线程这样的一个过程

补充:

1. 进程等待回收资源的时候,会有一个参数用来获取子进程的退出时的退出信息,包括有信号等等内容,那线程为什么没有?

这个问题其实不难理解,进程退出会有对应的信息是因为父进程不知道子进程的退出情况是什么,可能是正常退出也可能是异常退出,但是在今天看来,主线程并不关心这个问题,原因在于如果新建的进程出现了异常,那么直接整个进程都退出了,所以只要这个线程返回,那么这个线程必然是正常进行退出的,因此从这个角度来讲,确实不需要用来存储异常的信息

2. 如果子线程一直不退出,主线程要一直在这里等待吗?

答案确实是如此,因为这个调用是一个阻塞形式的等待,所以主线程必须卡在这里等这个新线程的返回信息,默认情况下这是阻塞等待,当然也有特殊情况,比如可以对这个新线程设置一个分离状态,表示主线程此时不关心新线程的退出情况了,可以退也可以不退,这个就叫做分离状态

那如何进行线程的分离?

方法也很简单,只需要调用对应的系统调用即可:

在这里插入图片描述

3. 线程的取消

在这里插入图片描述
线程的取消如何理解?简单来说就是当线程正在运行的时候把它进行取消,下面用代码来实现:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;class pthread_exit_info
{
public:pthread_exit_info(int code, const string &info): _code(code), _info(info){}public:int _code;string _info;
};// 新线程
void *ThreadRoutine(void *arg)
{const char *threadname = (const char *)arg;while (true){cout << threadname << " thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;sleep(1);}cout << threadname << " thread exit" << endl;return (void *)new pthread_exit_info(0, "exit normal");
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");cout << "main thread: "<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;sleep(2);cout << "线程取消" << endl;int n = pthread_cancel(tid);void *ret = nullptr;pthread_join(tid, &ret);std::cout << "main thread join done,"<< " n: " << n << " thread return: " << (int64_t)ret << std::endl;return 0;
}

在这里插入图片描述

那在线程被取消后,线程取消函数会返回一个0,而获取线程的退出信息会获取到一个-1,这个-1代表的是一个宏,表示这个线程是因为被线程取消了而退出的:

在这里插入图片描述
基于上述的内容可以看出,线程是可以被取消的,线程是一个死循环,但是依旧不影响它可以被取消,如果这个被取消的线程处于分离状态,那么这个线程被取消后会被操作系统自动回收掉对应的资源

关于原生线程库问题

结束了上面的话题,那么下面一个话题是,关于Linux的原生线程库的问题

在Linux操作系统中,操作系统内部的线程不是真线程,而是用进程模拟的线程,所以Linux系统中不会直接提供对应的任何关于创建线程的系统调用,最多是提供创建轻量级进程的系统调用,但是对于正常的操作系统来说,操作系统是有必要提供对应的线程的,那如何处理这个问题呢?所以就在Linux的系统和用户之间建立起来了一个软件层,这个软件层的作用之一就是负责在底层调用为系统创建对应的轻量级进程的接口,其次在上层返回的时候是一个操作线程的接口,比如说有创建线程,终止线程等等

所以在Linux系统中关于线程的一个解决方案是,使用了一个线程的库来解决的,这个原生线程库的意思就是系统会自带有这个库,如果没有这个库就不能正常运行和线程相关的进程

站在用户层的角度来讲,在用户创建了5个线程后,那么在系统中就会创建5个轻量级进程与之对应,所以在进行查询的时候就能找到5个对应的LWP,对于轻量级进程的管理操作系统自己就可以做到,所以直接复用之前的代码就可以了,但是对于用户来讲却并不是这样,用户想知道现在创建了多少个线程,每一个线程的状态是什么,当前有多少个,某个线程的退出信息是什么,这些该如何获取?从刚才的代码中可以看出可以通过join函数来获取,但是join函数的底层又该从哪里获取这些信息呢?

换句话说,Linux的线程是叫做用户级线程,从字面意思可以看出,这个线程是用户层上的概念,而在内核的角度看只有只有轻量级进程的概念,那操作系统是可以有这个能力把轻量级进程管理起来的,这是操作系统的本职工作,但是对于系统的原生库来说,它又该如何获取信息呢?它该如何进行管理信息呢?

结论是,当操作系统中如果存在了大量的轻量级进程,那么注定对应的原生库也会有自己的方法来进行管理,所以就引出了一个叫做tcb的概念,tcb是对标PCB的一个概念,可以理解成是一个线程描述符的概念,在这个tcb中必然会存在很多的信息,其中就包括有LWP这样的概念,因此换句话说可以理解成,在这样的库中就会存在一些封装的概念,同时还会为每一个线程封装一个简单的tcb,当然这个概念在Linux中是没有的

因此会引入下图这样的概念,类似于文件库,文件库中是可以申请有一段空间作为文件缓冲区,所以线程其实也有这样的能力,而下图中的线程栈就是这样的概念:

主线程的栈通常是在程序启动时由操作系统分配的真实的栈空间,它是在程序的虚拟地址空间中预留的一部分。这个栈空间是在程序的内存映像中分配的,因此可以被认为是在真实的栈上。

而对于其他创建的新线程,它们的栈空间可能由堆空间代替而形成,意味着它们的栈空间是在堆上动态分配的。这种动态分配使得线程栈的大小可以根据需要进行调整,并且可以更灵活地管理内存。

在这里插入图片描述

库是要被加载到共享区的,加载到共享区之后就会在这块区域内维护一个区域,这个区域其实就会分配一段空间,然后把这段空间的地址放到这个库当中,那么在线程进行运行的时候就可以在库中的指针找到这个线程所对应的地址空间了,换句话说,如果未来想要把这个线程进行退出,实际上就是把线程终止,把轻量级进程终止,把线程所维护的这段空间释放掉,就完成了释放的工作

那如果此时创建了很多线程呢?这些线程都会在这个库中进行维护:线程库通常会维护一个数据结构,比如线程表,用于存储每个线程的信息。这个表中的每个条目可能包含线程的标识符(如线程ID)、状态、优先级、调度相关信息,以及指向线程控制块(TCB)或线程栈的指针等。这样的线程表使得线程库可以有效地跟踪和管理每个线程的状态和资源

重要的是,线程控制块(TCB)通常包含了线程的所有信息,包括线程的上下文、寄存器状态、线程栈指针、局部存储等。因此,线程表中存储的指针指向每个线程的TCB,而TCB中又包含了线程的所有信息,使得线程库可以在需要时访问和修改线程的相关信息

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/519265.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于springboot+vue的高校教师电子名片系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

OJ习题之——圆括号编码

圆括号编码 1.题目描述2.完整代码3.图例演示 1.题目描述 题目描述 令Ss1 s2 …sn是一个规则的圆括号字符串。S以2种不同形式编码&#xff1a; &#xff08;1&#xff09;用一个整数序列Pp1 p2 … pn编码&#xff0c;pi代表在S中第i个右圆括号的左圆括号数量。&#xff08;记为…

进程之舞:操作系统中的启动、状态转换与唤醒艺术

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua&#xff0c;在这里我会分享我的知识和经验。&#x…

nmaptocsv.py脚本无法处理结果的备用工具

python nmaptocsv.py -i test.nmap -f ip-fqdn-port-protocol-service-version。 一: 使用背景&#xff1a; 使用一些其他的端口扫描软件&#xff0c;指纹识别可能有些端口 如一次&#xff1a;11011端口是mysql数据库但是别的软件扫不出来是mysql&#xff0c;借助nmap对1101…

基类指针指向派生类对象,基类不带虚函数,子类带虚函数产生的异常分析

基类指针指向派生类对象&#xff0c;基类不带虚函数&#xff0c;子类带虚函数产生的异常分析 基类指针指向派生类对象&#xff0c;指针的起始地址一定是指向基类起始地址的 这种情况下&#xff0c;当基类没有虚函数&#xff0c;而子类存在虚函数时&#xff0c;就会出现问题&am…

lqb省赛日志[1/37]

一只小蒟蒻备考蓝桥杯的日志 文章目录 笔记mod进制转换轻轻崩溃前缀和 差分双指针&#xff08;夹逼&#xff09;人脑思维...->计算机思维历史遗留问题1--22年B组初赛历史遗留问题2--试题 基础练习 阶乘计算 刷题心得小结 笔记 mod 进制转换 进制转换——颠覆小蒟蒻认知了…

Java开发从入门到精通(一):Java的基础语法高阶

Java大数据开发和安全开发 &#xff08;一)Java的流程控制1.1 分支语句1.1.1 IF分支语句第一种IF语句第二种IF-ELSE语句第三种IF-ELSE IF-ELSE语句if语句使用的几个常见问题 1.1.2 switch分支语句switch分支的执行流程switch分支的导学案例:电子备忘录if、switch的比较&#xf…

回南天的解决方案

广东的回南天还是那么湿&#xff0c;各种短视频在秀。 墙上流水 楼顶滴水 厕所镜子看不到人 出门滑行 衣服永远是湿的 湿度100%&#xff01; 那么这个让人难受的回南天&#xff0c;除关门关窗&#xff0c;还有没有更好的解决方案&#xff1f;&#xff1f;&#xff08;小…

论文阅读_世界模型

1 2 3 4 5 6 7 8英文名称: World Models 中文名称: 世界模型 链接: https://arxiv.org/abs/1803.10122 示例: https://worldmodels.github.io/ 作者: David Ha, Jurgen Schmidhuber 机构: Google Brain, NNAISENSE, Swiss AI Lab, IDSIA (USI & SUPSI) 日期: 27 Mar 2018 引…

django中URL配置和视图渲染

前提&#xff1a; 使用django-admin startproject XXX创建了一个django项目【项目目录为project】 django-admin startproject project 一&#xff1a;控制器配置 在项目的根目录创建一个Controller目录&#xff0c;后续所有的控制器方法都放在此目录下 这里我们在Control…

vue基础教程(4)——深入理解vue项目各目录

博主个人微信小程序已经上线&#xff1a;【中二少年工具箱】。欢迎搜索试用 正文开始 专栏简介1. 总览2. node_modules3.public4.src5.assets6.components7.router8.stores9.views10.App.vue11.main.js12.index.html 专栏简介 本系列文章由浅入深&#xff0c;从基础知识到实战…

Vue+OpenLayers7入门到实战:OpenLayers7如何使用gifler库来实现gif动态图图片叠加到地图上

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7 前言 OpenLayers7本身不支持gif图片作为图标要素显示到地图上,所以需要通过其他办法来实现支持gif图片。 本章介绍如何使用OpenLayers7在地图上使用gifler库先生成canvas画板,然后通过canvas画板的重绘事件来重新渲染地图…