Linux操作系统篇:多线程

一. Linux中线程是怎么理解的

1.1 线程概念         

在Linux中,线程是在进程“内部”执行的,线程是处于进程的进程地址空间中运行,线程用到的资源都是进程的资源,线程是执行进程的一部分代码,线程是最小的执行流,执行流要执行就得有资源,资源是在物理内存中存储着,通过进程地址空间+页表的形式找到对应的资源。

1.2 重新定义线程和进程

一个或多个执行流(线程),进程地址空间,文件描述符表,页表,物理内存保存的进程资源等加一起才是进程,线程是在进程内部的执行流资源。线程是操作系统调度的基本单位。进程是资源分配的基本实体(单位)。                                                                                                                         操作系统以进程为基本单位分配资源,进程在创建时就会携带一个线程,这个线程是是主线程。进程包含线程,进程内有一个或多个线程,一个进程有自己的一个PCB,页表,进程地址空间,文件描述符表,这都是操作系统给其分配的。

除了Linux操作系统外,管理线程是用结构体TCB描述线程并用链表或更高级的数据结构管理起来进程的线程们,TCB是置在进程的PCB中的。                                                                                而在Linux中,管理线程也是复用了管理进程的方法,在Linux中,并没有线程的概念,线程又称轻量级进程,每个线程都有自己的PCB,在之前讲进程的时候,在进程被创建的时候会有自己的一个PCB,其实这个PCB是属于进程被创建时一并被创建的主线程的。

1.3 线程周边概念

 1.3.1进程VS线程

1) 线程的创建和释放比进程的创建和释放更轻量级:更快。因为线程的创建不用创建进程地址空间,页表什么的,只需要创建一个PCB就可以,释放也是如此,只用释放一个PCB即可。

2)线程的切换比进程的切换也更轻量化:线程的切换不需要大规模的改动,只用替换CPU寄存器的数据即可,而进程的切换涉及到页表,进程地址空间等等资源的切换。还有一点是cache缓存,在CPU拿取数据是从cache拿取数据的,不会直接存内存拿取数据,内存数据是要先加载到cache中的,线程切换时,不用切换cache缓存的数据,因为进程中的线程共用一个进程地址空间,所以用到的物理内存的数据也是同一份数据。而进程的切换需要涉及到cache缓存中缓存数据的切换,这也是一个细节。

线程的优点
        创建线程的代价比进程小且线程占用的资源要比进程少很多。
        线程之间的切换需要操作系统做的工作要少很多。

线程的缺点                                                                                                                                              健壮性低下如果一个线程出现问题,这个进程将会崩溃。这是因为线程出异常,就是进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
        编程难度提高编写与调试一个多线程程序比单线程程序困难得多。

线程最重要的两个字段是线程自己的栈空间和硬件上下文,独立的硬件上下文能体现出线程是被调度处理的基本单元,栈能让线程的资源独立。线程除了栈空间外,其他区域都是共享的。进程的第一个线程是主线程,其他线程是工作线程,进程被创建出来就有一个线程(主线程),这个主线程负责执行main函数。

二. 线程控制

Linux并没有给用户提供操作线程的系统调用接口,因为Linux并没有线程的概念,而是轻量级进程的概念。但是Linux开发人员在应用层提供了线程库pthread(实现了对轻量级进程接口的封装)。pthread库几乎在所有的Linux版本中都自带,但毕竟是第三方库,在编译时要引入头文件<pthreaad.h>并且指定 -l pthread。      

2.1. 线程常用接口

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

作用:创建线程接口

参数说明:                                                                                                                              thread:获取创建成功的线程ID,该参数是一个输出型参数。
attr:用于设置创建线程的属性,一般是传入nullptr表示使用默认属性。
start_routine:该参数是一个函数指针,线程启动后要执行的函数。
arg:传给线程例程的参数。
返回值说明:                                                                                                                                   线程创建成功返回0,失败返回错误码。

主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行创建时给他传的函数指针对应的函数,而主线程则继续执行后续代码。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* Routine(void* arg)
{char* msg = (char*)arg;while (1){printf("I am %s\n", msg);sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid, NULL, Routine, (void*)"thread 1");while (1){printf("I am main thread!\n");sleep(2);}return 0;
}

当我们用ps axj命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的。

使用ps -aL命令,可以显示当前的轻量级进程。

  • 默认情况下,不带-L,看到的就是一个个的进程。
  • -L就可以查看到每个进程内的多个轻量级进程。

其中,LWP(Light Weight Process)就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们属于同一个进程。                                                                                     在Linux中,LWP确实是线程的ID,但是LWP是给操作系统内核使用的,操作系统调度线程的时候采用的是LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。                                                           篇thread_create函数的第一个参数是输出函数,获取线程的ID,不过这个ID不是LWP,因为LWP是给操作系统调用使用的,这个ID是给用户使用的,和LWP不一样,其实是在进程地址空间中的共享区中pthread库分配给线程的储存空间的首地址。(下面详解)。

pthread_t pthread_self(void);

获取线程的ID

 

调用pthread_self函数即可获得当前线程的ID,类似于调用getpid函数获取当前进程的ID。

例如,下面代码中在每一个新线程被创建后,主线程都将通过输出型参数获取到的线程ID进行打印,此后主线程和新线程又通过调用pthread_self函数获取到自身的线程ID进行打印。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;while (1){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);}
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}while (1){printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());sleep(2);}return 0;
}

注意: 用pthread_self函数获得的线程ID与内核的LWP的值是不相等的,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,它们之间是一对一的关系。

int pthread_join(pthread_t thread, void **retval);

线程等待

首先需要明确的是,一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的。如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的问题,也就是内存泄漏。

参数说明:

  • thread:被等待线程的ID。
  • retval:线程退出时的退出码信息。不关心可设置为nullptr。

返回值说明:

  • 线程等待成功返回0,失败返回错误码。
void pthread_exit(void *retval);

终止线程

参数说明:

  • retval:线程退出时的退出码信息。

说明一下:

  • 该函数无返回值,跟进程一样,线程结束的时候无法返回它的调用者(自身)。
  • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。

使用pthread_exit函数终止线程,并将线程的退出码设置为6666。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;}pthread_exit((void*)6666);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %d\n", i, tid[i], (int)ret);}return 0;
}

 

int pthread_cancel(pthread_t thread);

使用pthread_cancel函数取消某一个线程

参数说明:

  • thread:被取消线程的ID。

返回值说明:

  • 线程取消成功返回0,失败返回错误码。

线程是可以取消自己的,取消成功的线程的退出码一般是-1。

int pthread_detach(pthread_t thread);

 分离线程

默认情况下,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。但如果我们不关心线程的返回值,join也是一种负担,此时我们可以将该线程进行分离,后续当线程退出时就会自动释放线程资源。一个线程如果被分离了,这个线程依旧要使用该进程的资源,依旧在该进程内运行,甚至这个线程崩溃了一定会影响其他线程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源。

参数说明:

  • thread:被分离线程的ID。

返回值说明:

  • 线程分离成功返回0,失败返回错误码。

例如,下面我们创建五个新线程后让这五个新线程将自己进行分离,那么此后主线程就不需要在对这五个新线程进行join了。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{pthread_detach(pthread_self());char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;}pthread_exit((void*)6666);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}while (1){printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());sleep(1);}return 0;
}

三. 线程在原生线程库中的ID到底是什么?

 进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时该进程内的所有线程都是能看到这个动态库的。

我们说每个线程都有自己私有的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈空间就是在共享区中开辟的。除此之外,线程库在进程地址空间的共享区内回为每个线程创建一个TCB保存线程的信息,TCB包含struct pthread,线程局部存储,栈空间。每个线程都有自己的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。
每一个新线程在共享区都有这样一块区域对其进行描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息。

 

线程ID本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程。 线程ID就是自己在进程地址空间共享区TCB的首地址。

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

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

相关文章

Android记录3--ExpandableListView使用+获取SIM卡状态信息

布局文件&#xff1a; /SIM_Card_Demo/res/layout/inbox.xml <LinearLayout xmlns:android“http://schemas.android.com/apk/res/android” xmlns:tools“http://schemas.android.com/tools” android:layout_width“match_parent” android:layout_height“match_par…

【代码】python实现一个BP神经网络-原理讲解与代码展示

​ 本文来自《老饼讲解-BP神经网络》https://www.bbbdata.com/ 目录 一、BP神经网络原理回顾1.1 BP神经网络的结构简单回顾1.2.BP神经网络的训练算法流程 二、python实现BP神经网络代码2.1.数据介绍2.2.pytorch实现BP神经网络代码 在python中要如何使用代码实现一个BP神经网络呢…

使用Rsbuild构建基于Vue3+Vant4开发h5应用

目录 一、介绍 1.1 Vant介绍 1.2 Rsbuild介绍 1.3 Vue介绍 二、构建应用 1.第一步 2.第二步 3.第三步 4.第四步 5.第五步 6.在项目中使用 Vant4 组件 7.移动端适配Rem 8. 执行 cnpm run dev 启动项目 一、介绍 1.1 Vant介绍 Vant 是一个轻量、可定制的移动端组…

单机小游戏好上架的应用市场有哪些?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

Vue3中的常见组件通信(超详细版)

Vue3中的常见组件通信 概述 ​ 在vue3中常见的组件通信有props、mitt、v-model、 r e f s 、 refs、 refs、parent、provide、inject、pinia、slot等。不同的组件关系用不同的传递方式。常见的撘配形式如下表所示。 组件关系传递方式父传子1. props2. v-model3. $refs4. 默认…

Mac电脑FTP客户端推荐:Transmit 5 for Mac 中文版

Transmit 5是一款专为macOS平台设计的功能强大的FTP&#xff08;文件传输协议&#xff09;客户端软件。Transmit 5凭借其强大的功能、直观易用的界面和高效的性能&#xff0c;成为需要频繁进行文件传输和管理的个人用户和专业用户的理想选择。无论是对于新手还是经验丰富的用户…

Starlink全系卫星详细介绍,波段频谱、激光星间链路技术、数据传输速率等等

Starlink全系卫星详细介绍&#xff0c;波段频谱、激光星间链路技术、数据传输速率等等。 Starlink是SpaceX公司开发的一个低轨道&#xff08;LEO&#xff09;卫星网络系统&#xff0c;旨在为全球用户提供高速宽带互联网服务。截至2024年6月&#xff0c;Starlink已经发射并运行…

终于找到了免费的云服务器

今天朋友推荐了一个免费的云服务器&#xff1a;“阿贝云” 我最喜欢的是它的"免费虚拟主机"“免费云服务器”&#xff0c;省了我好多钱&#xff0c;我的使用感受是用起来经济实惠省心&#xff0c;不要钱的东西谁不喜欢呢&#xff0c;对于普通开发者来说&#xff0c;…

长尾式差分放大电路调零

长尾式放大电路用了两个参数相同的三极管&#xff0c;但实际上并没有完全相同的三极管&#xff0c;所以为了提高差分放大电路的对称性(一边电流增加多少&#xff0c;另一边电流减小多少&#xff0c;即能在电阻Re上产生的压降不变(后面做虚地处理))&#xff0c;在下图中加入可调…

【Linux 杂记】TOP命令

top命令用于动态显示系统中正在运行的进程的详细信息&#xff0c;以及系统的整体资源使用情况。以下是其主要输出解释&#xff1a; Header 表头信息&#xff1a; top&#xff1a;当前时间和运行时间。Tasks&#xff1a;进程统计信息&#xff0c;如总进程数、运行中、睡眠中等。…

xocde编辑器支持修改为中文吗?不支持

xocde编辑器支持修改为中文吗&#xff1f; 不支持

rttys服务器和客户端

rttys服务器 1.下载 https://github.com/zhaojh329/rttys/releases2.解压运行 libev交叉编译 cd libev ./configure --hostarm-linux CCaarch64-poky-linux-gcc --prefix/home/michael/rtty_install make install DESTDIR/home/michael/rtty_installrtty客户端 1.git地…

RabbitMQ —— 理解及应用场景

一、MQ相关的概念 RabbitMQ 是一种分布式消息中间件&#xff0c;消息中间件也称消息队列MQ&#xff0c;那么什么是MQ呢&#xff1f;请继续阅读下文。 1.1、MQ的基本概念 什么是MQ MQ(message queue)&#xff0c;从字面意思上看就个 FIFO 先入先出的队列&#xff0c;只不过队列…

2024 年解锁 Android 手机的 7 种简便方法

您是否忘记了 Android 手机的 Android 锁屏密码&#xff0c;并且您的手机已被锁定&#xff1f;您需要使用锁屏解锁 Android 手机&#xff1f;别担心&#xff0c;您不是唯一一个忘记密码的人。我将向您展示如何解锁 Android 手机的锁屏。 密码 PIN 可保护您的 Android 手机和 G…

Node.js中基于node-schedule实现定时任务之详解

文章目录 一、定时任务二、node-schedule、1、安装2、引入3、基于Cron表达式的规则4、基于Date的规则5、基于RecurrenceRule的规则6、API7、状态监听 一、定时任务 实际工作中&#xff0c;可能会遇到定时清除某个文件夹内容&#xff0c;定时发送消息或发送邮件给指定用户&…

Django集成OpenAI

Django集成OpenAI 通过前面 django 框架的基本开发知识&#xff0c;我们现在可以开始在 django 上做稍微深一点当然应用开发了。 这一章开始编写怎么集成调用 openai &#xff0c;设置环境以及 openai 的基础知识。 大家都知道 ai 的多模态逐渐扩大&#xff0c;各种应用层出…

怎么采集阿里巴巴1688的商品或商家数据?

怎么使用简数采集器批量采集阿里巴巴1688的商品或商家相关信息呢&#xff1f; 简数采集器暂时不支持采集阿里巴巴1688的相关数据&#xff0c;谢谢。 简数采集器采集网络网页数据非常简单高效&#xff1a;输入要采集的网址&#xff0c;简数智能算法会自动提取出网页上的关键信…

Charles抓取安卓应用https包演示

一、准备软件 夜神安卓模拟器 (yeshen.com) Charles (charlesproxy.com) 二、配置抓包 2.1 Charles安装PC根证书 记住这里的ip端口 三、安卓模拟器配置 3.1 配置安卓客户端网络代理 填写上文的ip端口&#xff0c;保存 3.2 安装根证书 3.2.1 导出根证书 linux主机执行 op…

推荐4款实用工具,非常好用,建议收藏

PDFREAL PDFReal 是一个功能强大的在线PDF编辑工具&#xff0c;提供多种实用的PDF处理功能。用户可以在一个网站上完成包括PDF合并、PDF拆分、PDF压缩、PDF保护、PDF解锁等多种操作。此外&#xff0c;PDFReal 还支持将文本转换为PDF、将图片转换为PDF、添加水印、提取页面内容等…

基于Django、Bootstrap的电影推荐系统,算法基于用户的协同过滤算法,有爬虫有可视化后台

背景 基于Django和Bootstrap的电影推荐系统结合了用户协同过滤算法&#xff0c;通过爬虫技术获取电影数据&#xff0c;并在可视化后台展示推荐结果。该系统旨在提供个性化的电影推荐服务&#xff0c;帮助用户发现符合其喜好的电影。 用户协同过滤算法是一种常用的推荐算法&am…