【Linux】线程控制

文章目录

  • 📖 前言
  • 1. 线程的id
    • 1.1 pthread_self:
    • 1.2 线程独立栈结构:
    • 1.3 pthread_t究竟是什么:
    • 1.4 线程的局部存储:
  • 2. 线程退出的三种方式
    • 2.2 - 1 方式一:pthread_cancel
    • 2.2 - 2 方式二:pthread_exit
    • 2.2 - 3 方式三:隐式退出
  • 3. 线程的分离
    • 3.1 新线程分离后,主线程先退出:
    • 3.2 立即分离,但是还是join到了:
    • 3.3 线程退出的第四种方式:

📖 前言

上一章我们认识了什么是线程,对线程有个基本的概念,了解并学习了进程和线程的关系,对页表有了更深层次的理解,还对查看进程有了了解。
本章我们再继续学习线程,将对线程id进行了解,线程私有栈等,接下来学习线程控制,线程退出,线程分离等,目标确定,办好小板凳准备开讲了……


1. 线程的id

在这里插入图片描述

  • 线程创建和进程一样也得有线程id。其实十六进制打印出来的pthread_t tid,是个地址。

1.1 pthread_self:

谁调用这个函数,就把线程id返回:

在这里插入图片描述
返回值:

在这里插入图片描述

  • 在多线程程序中,如果一个变量定义在全局作用域内,则它是一个全局变量。
  • 全局变量具有全局可见性,可以被程序中的任何线程访问。
  • 因为全局变量存储在进程的数据段中,所有线程都可以访问这个数据段。

1.2 线程独立栈结构:

  1. 线程是一个独立的执行流。
  2. 线程一定会在自己的运行过程中,产生临时数据(调用函数,定义局部变量等)。
  • 所以线程一定需要有自己的独立的栈结构。
  • 代码区好划分,以函数的方式让线程各自执行一份。
  • 数据不需要划分,数据属于全局,所有线程可以共享。
  • 堆区每个线程可以自己去申请,虽然一个线程申请的堆区堆其他线程也是保持可见性的,但是通过保留地址(定义全局指针),可以对其他线程保持可见性。

像我们调用的pthread_create,pthread_join这些函数都不是我们自己实现的,但是我们可以使用它,是因为我们链接了线程库。

  • Linux的线程库,虽然是原生的,在系统当中会内置的线程库,但是依旧是用户级线程库。
  • 因为Linux没有真线程,就没办法提供真线程的调用接口。
  • 共享库(Shared Library)是被所有线程共享的。
    在这里插入图片描述
    用了动态库就要将动态库加载到内存里,映射到地址空间当中:
  • 我们使用的线程库,虚拟地址访问代码区,访问对应接口时
  • 可以在地址空间内进行跳转,跳转到共享区
  • 然后把对应的相关的创建线程的一大堆工作全部做完
  • 做完后把结果返回,此时线程也就被创建好了

所有的代码执行,都是在进程的地址空间当中进行执行的。

1.3 pthread_t究竟是什么:

libpthread. so是Linux系统中用于支持多线程编程的共享库,其中包含了与线程管理相关的函数和数据结构

Linux有轻量级进程,能够模拟实现线程,但是就只要靠内核当中的轻量级进程就能把线程的所有工作全做了吗?如果全部都做了,那么为什么不提供系统接口呢?

—— 其实并没有全部都做了。

  • 线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。

库可以创建多个线程,那么库要不要管理线程呢?答案是肯定的。

  • 要想管理就要,先描述,再组织。
  • 再库里面有一个struct thread_info结构体,是用来描述创建的线程的。
  • 这个结构体中有线程的tid,还有一个重要的就是指向线程私有栈的栈顶指针。

在这里插入图片描述
目前地址空间内只看到了一个栈。

pthread_t对应的就是用户级线程的控制结构体的起始地址:

  • 可以通过该地址找到线程相关属性。
  • 给创建线程的用户返回的是,线程控制结构体的起始地址。
  • 用户的所有创建线程、等待线程、获取线程的各种属性,都是这个库帮我们去做的,控制进程内的轻量级进程来完成的。
  • 每一个线程都有一个thread_info,而它的属性里面就有私有栈。
  • 所以最终的结论就是,主线程的独立栈结构,用的就是地址空间中的栈区,新线程用的栈结构,用的是库中提供的栈结构。
  • 在Linux内核中,每个进程都有一个相应的task struct结构体来描述进程的信息,而每个线程都是作为进程的一部分存在的,所以也有对应的task_struct结构体来描述线程的信息。
  • task_struct结构体中,有一个指向线程特定数据(Thread-Specific Data,TSD)的指针字段thread_infothread_info是一个指向thread_info结构体的指针,它包含了很多与线程执行相关的信息,其中包括线程的栈顶指针(Stack Pointer)`。
  • thread_info结构体中的栈顶指针字段通常称为sprsp,具体名称可能会因架构和内核版本而有所不同。通过访问thread_info结构体中的栈顶指针字段,可以获取当前线程的栈顶位置。
  • struct thread_info结构体存储在内核空间中,只能由操作系统内核访问和管理。它是内核用来追踪和管理线程状态的重要数据结构,用户空间的应用程序无法直接访问或修改它。
  • 总而言之,在Linux内核中, 每个线程的私有栈的栈顶指针通常是存储在对应线程的thread_info结构体中的一个字段中,可以通过该指针来获取线程栈的栈顶位置。

pthread_t到底是什么类型?取决于实现。
pthread_t类型的线程ID,本就是一个进程地址空间上的一个地址。

线程库在底层与内核进行交互,通过系统调用接口(如 clone)来创建和管理内核线程:

  • 每创建一个线程,库里面可不仅仅有代码,库里面也有数据。
  • 当用户层调用创建一个线程时,在库里面会为我们创建一个我们所对应的thread_info这样的结构线程控制块。
  • 里面有个指针指向用户空间的某个位置,代表曾经申请的栈空间,这个空间是怎么设置的,我们不管。
  • 实际上这个空间也是由用户传的,底层调用的是clone。

用户级线程:栈是由库来维护,但是还是用户提供的。

在这里插入图片描述
备注:

  • pthread_info不是在库里面直接创建的,而是由操作系统内核在创建线程时生成的线程控制块的信息。
  • 可以通过一些系统调用或库函数来获取线程的相关信息,如线程ID、优先级等。
  • 具体的获取方式可能因操作系统和编程语言的不同而有所差异。
  • 线程是在库当中维护的,对应的执行流是由操作系统去维护,对应的线程相关的用户需要的一些属性是由库里面维护的,线程对应的私有栈是每一个线程控制结构保存的。
  • 线程对应的pthread_t id是线程控制结构的起始地址。

最终结论:

  • 主线程的独立栈结构,用的就是地址空间中的栈区,新线程用的栈结构,用的是库中提供的栈结构。
  • Linux中,线程库用户级线程库,和内核的LWP是1 : 1
  • 一个用户级线程,一个内核的LWP。

pthread_ t本质是个地址,是pthread原生线程库被load到内存,并映射进当前进程的地址空间之后,地址空间里在线程库内会为我们当前线程创建对应线程的相关信息。
比如说,线程的描述结构体,线程的局部存储,线程的独立栈,主线程用的依旧是地址空间的栈区,而新线程用的则是共享区当中,以及在用户空间当中给我们提供的对应的栈区。
一个线程要是出异常,整个进程都挂掉了。
也可以在线程内部malloc空间,也可以将对应的结果数据返回,新线程和主线程之间,两个线程之间要进行值的交换是可以通过堆区进行交互的,不定非要硬传参数。
主线程创建的对象,在新线程依旧能看到,这说明,在多个线程里面地址空间内,堆空间也是有可见性的。

1.4 线程的局部存储:

  • 线程的局部存储,代表的是,我们的线程除了保存临时数据时有自己的线程栈,我们的ptread库还提供了一种能力:
    • 如果我们定义了一个全局的变量,但是这个全局的变量想让每个线程各自私有,那么就可以使用线程局部存储这样的概念。
    • 在全局变量前加上__thread即可。
    • 可以理解成将这个全局变量都拷贝一份给对应的线程,所以每个线程都有自己的。

2. 线程退出的三种方式

2.2 - 1 方式一:pthread_cancel

代码演示:

#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;static void printTid(const char* name, const pthread_t& tid)
{printf("%s 正在运行,thread id: 0x%x\n", name, tid);
}void* startRoutine(void* args)
{const char* name = static_cast<const char*>(args);int cnt = 5;while (true){// cout << "线程正在运行..." << endl;printTid(name, pthread_self());sleep(1);// if (cnt-- == 0) break;// if (cnt-- == 0)// {//     int* p = nullptr;//     *p = 100; // 野指针问题// }}// 新线程运行5s后退出cout << "线程退出啦……" << endl;// 1. 线程退出的方式: return// 通过pthread_join获得这个函数的返回值// 返回值是个void*,所以为了拿到这个void*需要传进去void**// 一个输出型参数// 2. 线程退出方式,pthread_exitpthread_exit((void*)111);// return (void*)10;
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void*)"thread1");// 代表main thread对应的工作,一创建就取消sleep(3);// 3. 线程退出方式,给线程发送取消请求,如果线程是被取消的,退出结果是: -1pthread_cancel(tid);cout << "new thread been canceled" << endl;// cout << "new thread id: " << tid << endl; // 线程ID -- 为什么这么大?// PTHREAD_CANCELED;// 主线程运行10s后退// sleep(10);// 线程退出的时候,一般必须要进行join,如果不进行join// 就会造成类似于进程那样的内存泄漏的问题(没有僵尸线程这样的说法)// 线程对应的退出结果暂时不获取void* ret = nullptr;pthread_join(tid, &ret); // void** retval是一个输出型参数cout << "main thread join success, *ret: " << (long long)ret << endl;// sleep(10);while (true){cout << "main thread 正在运行..." << endl;printTid("main thread", pthread_self());sleep(1);}return 0;
}
  • 新线程被创建出来就如5s后就会退,退出之后,看到的线程对应的信息应该是还有的。
  • 因为还没有被join,5s后被join后当我们主线程开始打印消息时,说明join完成。
  • 我们看到的现象是线程退出时,查看时却少了一个,预期应该是两个线程,应该等到join之后才变成一个线程。
  • ps命令在查的时候,那种退出的线程就不显示了。

取消一个线程:

在这里插入图片描述
返回值:

在这里插入图片描述

  • 我们上述代码是主线程取消新线程,但是如果要是反过来呢,用新线程取消主线程可以吗?答案是可以的,但是不推荐这种做法。
    • 如果新线程取消了主线程(调用了pthread_cancel函数取消主线程),主线程会立即停止执行,并开始清理资源。
    • 取消主线程可能导致未完成的操作无法完整执行,因此需要在编写线程代码时小心处理取消请求。
    • 我这边自己的验证是,主线程退出了,但是剩余的线程依旧在执行。

正常退出:

在这里插入图片描述
取消后退出:

在这里插入图片描述

2.2 - 2 方式二:pthread_exit

退出线程:

在这里插入图片描述

  • pthread_exit是一个线程函数的库函数,可以让线程提前退出执行并返回一个指定的退出码。
  • 调用pthread_exit函数会立即终止当前线程的执行,而不管该线程是否已经完成其任务。
  • 因此,在调用pthread_exit函数之前,需要确保所有资源已经正确地释放。

当调用pthread_join函数等待一个线程结束时:

  • 如果被等待的线程通过pthread_exit函数退出,则该线程的返回值会被传递给pthread_join函数的第二个参数所指向的变量。

如果使用exit(1),那么整个进程就退出了。

2.2 - 3 方式三:隐式退出

当线程函数执行完毕并返回时,线程会隐式地退出。在这种情况下,线程的退出码默认为0,表示正常终止。

对于线程的隐式退出,即通过线程函数显式返回一个值(使用return语句),是可以被pthread_join获取到的。当线程通过显式返回方式终止时,其返回值会被传递给pthread_join函数。


3. 线程的分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

一个线程要是被分离了,就不能被join了。分离和join是矛盾的。新线程不退出,主线程就卡在那里,等待新线程join。

joinable是可被等待的状态。

在这里插入图片描述

#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <sys/syscall.h>
#include <unistd.h>using namespace std;// 只要是带了__thread,可以理解成将这个全局变量都拷贝一份给对应的线程,所以每个线程都有自己的
int global_value = 100;void* startRoutine(void* args)
{// 线程分离:(自己分离自己)// pthread_detach(pthread_self());// cout << "线程分离了....." << endl;while (true){cout << "thread " << pthread_self() << " global_value: "<< global_value << " &global_value: " << &global_value << " Inc: " << global_value++ << " lwp " << syscall(SYS_gettid) << endl;sleep(1);}// 退出进程,任何一个线程调用exit,都表示整个进程退出// exit(1);// pthread_exit()
}int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void*)"thread1");pthread_create(&tid2, nullptr, startRoutine, (void*)"thread2");pthread_create(&tid3, nullptr, startRoutine, (void*)"thread3");// sleep之后才能看到join失败的情况// sleep是为了这三个线程都detach之后再进行分离sleep(1);// 因为主线程和新线程谁先被调度是不一定的,有可能,join先被阻塞挂起了,而新线程还没有detach// 更倾向于:让主线程,分离其他线程pthread_detach(tid1);pthread_detach(tid2);pthread_detach(tid3);int n = pthread_join(tid1, nullptr);cout << n << ":" << strerror(n) << endl;pthread_join(tid2, nullptr);cout << n << ":" << strerror(n) << endl;pthread_join(tid3, nullptr);cout << n << ":" << strerror(n) << endl;return 0;
}

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。

线程分离的情况:

  1. 立即分离(压根不关系线程执行的怎么样)。
  2. 延后分离(线程跑一段时间后再分离)。
  • 新线程分离,但是主线程先退出(进程退出)。一般我们分离线程时,对应的主线程一般不要退出(常驻内存的进程)。
  • 一旦主线程退出了,所有新线程也会跟着释放,主线程退出代表进程退出,地址空间页表、代码、数据等都会释放。
  • 新线程退出了则不会影响进程的继续运行。
  • 线程分离了,线程退出就会自动被系统回收。
  • 这就意味着,我们不再关心这个线程的死活。

3.1 新线程分离后,主线程先退出:

将新线程全部都分离,然后主线程中也不去join新线程了,主线程执行完,直接退出:

运行结果:

在这里插入图片描述
我们发现:三个新线程一并退出了。

进程退出的时候,操作系统就回收了这个进程的程序地址空间,连资源都被释放了,线程就没有办法继续运行,自然就退出了。

为了避免这种现象,一般我们分离线程的时候,都倾向于让主线程保持在后台运行(常驻内存的程序)。

3.2 立即分离,但是还是join到了:

void *startRoutine(void *args)
{// 线程分离:(自己分离自己)pthread_detach(pthread_self());cout << "线程分离了....." << endl;while (true){cout << "thread " << pthread_self() << " global_value: "<< global_value << " &global_value: " << &global_value << " Inc: " << global_value++<< " lwp " << syscall(SYS_gettid) << endl;sleep(1);}// 退出进程,任何一个线程调用exit,都表示整个进程退出// exit(1);// pthread_exit()
}
  • 这是因为,我们创建一个线程的时候,它会先去执行线程创建的相关代码。
  • 此时主线程,又直接去执行后面的代码了。
  • 此时pthread_join的调用是成功的,因为线程自己的detach代码还没有被执行!

此时只需要将主线程在分离之后等等,再去join。

3.3 线程退出的第四种方式:

线程分离之后,延后退出。

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

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

相关文章

Vue3封装知识点(三)依赖注入:project和inject详细介绍

Vue3封装知识点&#xff08;三&#xff09;依赖注入&#xff1a;project和inject详细介绍 文章目录 Vue3封装知识点&#xff08;三&#xff09;依赖注入&#xff1a;project和inject详细介绍一、project和inject是什么二、为了解决什么问题三、project和inject如何使用1.provid…

51单片机DS1302万年历时钟温度12864显示仿真( proteus仿真+程序+报告+讲解视频)

51单片机DS1302万年历时钟温度12864显示仿真 1.主要功能&#xff1a;2.仿真3. 程序代码4. 设计报告5. 设计资料内容清单 51单片机DS1302万年历时钟温度12864显示仿真( proteus仿真程序报告讲解视频&#xff09; 仿真图proteus7.8及以上 程序编译器&#xff1a;keil 4/keil 5 …

fastadmin 在操作里加订单详情弹窗

第一步找到控制器对应的js文件。 //添加一个详情按钮 buttons:[{name: detail,text: 详情,title: 详情,icon: fa fa-list,extend: data-area\["80%", "80%"]\,classname: btn btn-xs btn-primary btn-dialog,url: order/detail }], 在order控制器添加方法&…

Vue中一键批量注册全局组件

文件目录如下 1. component文件夹中编写所有的公共组件 注意&#xff1a;之后一键注册的全局组件名就是每个公共组件&#xff08;xxx.vue&#xff09;文件的文件名 xxx 2. plugins/components.js中批量注册组件 import Vue from "vue"let requireFile require.con…

[NLP] LLM---<训练中文LLama2(二)>扩充LLama2词表构建中文tokenization

使用SentencePiece的除了从0开始训练大模型的土豪和大公司外&#xff0c;大部分应该都是使用其为当前开源的大模型扩充词表&#xff0c;比如为LLama扩充通用中文词表&#xff08;通用中文词表&#xff0c;或者 垂直领域词表&#xff09;。 LLaMA 原生tokenizer词表中仅包含少量…

企业架构LNMP学习笔记45

失效机制&#xff08;了解&#xff09; 1&#xff09;如果key过期了&#xff0c;value会及时删除么&#xff1f;空间会及时清理么&#xff1f; 2&#xff09;如果分配的存储空间&#xff0c;写满了&#xff0c;还允许写么&#xff1f; -m可以配置内存大小。 memcached 内部不…

Cesium 地球(2)-瓦片创建

Cesium 地球(2)-瓦片创建 QuadtreePrimitive代码执行4个步骤: step1: update()step2: beginFrame()step3: render()step4: endFrame() 但并不是瓦片的创建步骤。 1、创建 QuadtreeTile 基于 step3: render() step3: render()┖ selectTilesForRendering()在 selectTilesFo…

laravel框架 - 安装初步使用学习 composer安装

一、什么是laravel框架 Laravel框架可以开发各种不同类型的项目&#xff0c;内容管理系统&#xff08;Content Management System&#xff0c;CMS&#xff09;是一种比较典型的项目&#xff0c;常见的网站类型&#xff08;如门户、新闻、博客、文章等&#xff09;都可以利用CM…

Python解析MDX词典数据并保存到Excel

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 察纳雅言&#xff0c;深追先帝遗诏&#xff0c;臣不胜受恩感激。 原始数据和处理结果&#xff1a; https://gitcode.net/as604049322/blog_data/-/tree…

【Unity插件】实现多人在线游戏——Mirror插件的使用介绍

文章目录 前言导入Mirror插件 简单介绍一、RPC调用二、错误注意 基本使用一、创建场景的网络管理器二、创建一个玩家三、添加玩家初始生成位置四、玩家控制五、同步摄像机六、同步不同角色的名字和颜色修改七、同步动画八、同步子弹方法一方法二 九、聊天功能十、场景同步切换十…

现代循环神经网络-门控循环单元(GRU)

理论 门控隐状态 门控循环单元与普通的循环神经网络之间的关键区别在于&#xff1a; 前者支持隐状态的门控。 这意味着模型有专门的机制来确定应该何时更新隐状态&#xff0c; 以及应该何时重置隐状态。 这些机制是可学习的&#xff0c;并且能够解决了上面列出的问题。 例如&…

双目立体视觉

#理想模型图 其中&#xff1a; b 为基线&#xff0c;即两个相机原点之间的距离 &#xff1b; fl和fr 分别为左右相机的焦距&#xff0c;理想的双目相机flfrf &#xff1b; cl和cr 分别为左右相机的光心&#xff0c;理想的双目相机clcr &#xff1b; xl和xr 分别为空间中的一点…

2023/09/15 qt day1

代码实现图形化界面 #include "denglu.h" #include "ui_denglu.h" #include <QDebug> #include <QIcon> #include <QLabel> #include <QLineEdit> #include <QPushButton> denglu::denglu(QWidget *parent): QMainWindow(p…

【AI】机器学习——支持向量机(非线性及分析)

5. 支持向量机(线性SVM) 文章目录 5.4 非线性可分SVM5.4.1 非线性可分问题处理思路核技巧核函数特点 核函数作用于SVM 5.4.2 正定核函数由 K ( x , z ) K(x,z) K(x,z) 构造 H \mathcal{H} H 空间步骤 常用核函数 5.5 SVM参数求解算法5.6 SVM与线性模型关系 5.4 非线性可分SVM …

聊天机器人

收集窗帘相关的数据 可以用gpt生成&#xff0c;也可以用爬虫 图形化界面 gradio 向量数据库 faiss python代码 import gradio as gr import random import timefrom typing import Listfrom langchain.embeddings.openai import OpenAIEmbeddings from langchain.vectorstor…

揭秘:WhatsApp的注册策略

WhatsApp账号的注册方式可以分为两种&#xff1a;实体卡注册和虚拟卡注册。实体卡注册是指使用个人手机卡完成注册&#xff0c;而虚拟卡注册则通过前面提到的对接平台来完成的。 账号注册问题一直是导致WhatsApp账号永久封禁的主要原因。由于WhatsApp广泛为群发获客等用途之一…

设计方法编写测试用例---思路分析

测一四年我在YX公司带测试团队&#xff0c;一个用例评审的会议上&#xff0c;一不小心超常发挥&#xff0c;结果卡在了一个用例设计方法上&#xff0c;印象非常深刻&#xff0c;当时的业务场景是支付方式的选择和优惠方案。 在后来的工作中&#xff0c;也曾几次遇到需要选择合…

Docker 安装

Docker 官网&#xff1a;Docker: Accelerated Container Application Development Docker Hub官网&#xff1a;https://hub.docker.com/ 前提说明 CentOS Docker 安装 前提条件 目前&#xff0c;CentOS 仅发行版本中的内核支持 Docker。Docker 运行在CentOS 7 (64-bit)上&…

软件定制开发具有以下特点|APP搭建|小程序

软件定制开发具有以下特点|APP定制|小程序 一、快速响应用户需求 软件定制开发的优势在于&#xff0c;它可以快速响应用户的需求&#xff0c;因为它是在现有软件的基础上进行功能定制、界面定制、服务定制等改造&#xff0c;而不是从零开始进行重新设计与开发&#xff0c;所以…

vscode快捷键大全中英文

vscode快捷键大全中英文 源文件下载链接