线程详细讲解---聚焦linux

概念

进程=内核数据结构+代码和数据(执行流)。承担分配资源的基本实体。

线程:是进程内部的一个执行分值(执行流)。cpu调度 的基本单位。

线程可以用进程来模拟。

面对资源的划分本质就是对虚拟地址的划分。而虚拟地址就是资源的入口;

如何去降代码区划分,其实每一个函数就是虚拟地址的集合,就是让线程执行ELF程序的不同函数即可。

之前的进程实际上就是内部只有一个线程的进程。

因为线程和进程有很大的相似性,所以线程的描述块可以复用进程的,不用自己再重复设计。

linux视角,线程就是执行流

内核cpu视角:线程就是轻量级进程,或者说就是轻量级进程模拟实现的。

进程强调独占,部分共享(比如通信的时候)

线程强调共享,部分独占。

结论:线程在进程的地址空间内运行

操作系统在启动的时候创建中断映射表,创建struct page mem\[\](数组,每一个page都有对应的下标),每一个page都是4kb,操作系统加载磁盘资源到物理内存都是以4kb为单位的。

所以,申请物理内存就是在查数组,改page。

页表

如果说查询页表查询失败,可能发生写时拷贝等错误,此时需要申请内存,查询mem数组,找到没有被使用的page,修改标志位,填充物理页框地址。

写实拷贝,缺页中断,内存申请;背后都要建立新的页表和内存映射关系。

进程就是一张页目录和n张页表构建的映射关系。虚拟地址是索引是下标,物理页框地址是目标。

虚拟地址(低12位)+页框地址就是映射的物理地址,所以页表映射的不是物理地址而是页框地址,拿着虚拟地址的后12位当偏移量就能拿到物理地址。

执行流看到的资源,在合法的情况下拥有了多少的虚拟地址,虚拟地址就是资源的划分。

虚拟地址空间是mm_struct+vm_area_struct 本质就是进行资源的统计数据和整体数据。页表说白了就是虚拟到物理地址的一个地图。资源划分:本质是虚拟地址空间的划分。资源共享;本质就是虚拟地址的共享。

线程的深刻理解:

线程进行资源划分,本质就是划分地址空间,获得一定范围的合法虚拟地址。本质就是再划分页表

线程进行的资源共享本质就是页表条目的共享。

线程就是程序里的一个执行路线,或者说是进程内部的一个控制序列。编译的本质就是给每一条代码中的变量和函数进行编址。

将进程进行资源的合理分配和划分交给一个个的执行流来完成,这就是线程。

在cpu和linux中,线程都是更加轻量级的。

如果要进行虚拟到物理的访存一般都是cpu中的mmu来搞定,这个mmu中有一个部件能大大提高访存效率,就是TLB(转译后备缓冲器)

如何理解我们之前学的new,malloc.它们实质上是先申请虚拟地址空间,等什么时候真正要用到物理空间,才真正需要映射和申请。所以是物理地址的延迟申请。

写时拷贝拷贝的是页表。

线程的优点:

一句话总结:线程更加轻量化。

线程比进程节省资源(线程就是进程的一部分)。

资源管理,资源划分创建pcb之类的交给进程去完成 线程只进行资源的调度即可。

计算密集型应用,比如GPU,多个线程同时进行计算,不断刷新显示器。但是太多线程会增加切换线程的时间,而这个时间本来是可以拿来进行计算的。IO密集型应用:比如上传下载文件的时候,多个线程可以同时进行相应的任务能够大大节省等待的时间。

线程的切换要比进程之间的切换省事很多。

进程的切换要做的工作:1.cpu的硬件上下文需要切换,就是pcb的切换。cpu中的CR3寄存器切换的是页目录进而切换页表

线程的切换:线程的切换一般来说都是在同一个进程中进行的,因为不在同一个进程中的线程进行切换本质上还是进程间切换。所以线程之间的切换要比进程切换的步骤少很多。线程之间的切换不用进行cpu硬件上下文缓存保存清空,即线程之间的切换虚拟地址空间是相同的,不会切换页表。cpu中的TLB(虚拟物理映射关系缓存)和catch(代码段数据缓存)在进程之间切换时不会失效,大大减少了内核的操作量。

线程有哪些数据是自己私有的:

1.一组寄存器和线程的线程的上下文数据(线程能够被 独立调度)

2.独立的栈机构(线程是动态的,需要保存临时数据)

所以之前说的线程就是具有一个线程(一个执行流)的进程

Linux线程控制

验证理论

两个执行流同时在跑

创建线程的时候需要链接第三方动态库,所以在编写makefile的时候需要加-lname(动态库名字)

一个线程如果崩溃了,其他线程也跟着退出。这就是多线程健壮性低的原因之一。

LWP轻量级进程的id, PID进程id。我们平时的getpid(),实际上在线程中就是在获取lwp

当两个线程同时向显示器打印内容时,信息会混杂在一块,侧面说明了显示器本身就是一种共享文件,如果需要避免这种情况需要给显示器文件加锁。

pthread线程库

linux不提供真正意义上的创建线程的接口。用户只认线程,但是OS只认轻量级进程,为了统一使用方法,linux在用户和OS之间增加了一个软件层,pthread库就是在其中,他是OS原生线程库。

pthread库封装了创建轻量级进程的方法,给用户提供了一批创建线程的方法。

c++的多线程在linux下封装了pthread库。而在windows下封装了windows创建线程的接口。这就实现了语言的跨平台和可移植性。,

linux线程控制的接口

创建完之后需要进行线程等待pthread_join()

获取本线程的pthread_id,pthread_self()

线程传参和返回值

在多线程中间。全局变量和公共函数是线程之间共享的。

一个代码风格:

在强制类型转化的时候使用c++风格的编译时类型转化static_cast<强制转化的类型>,更安全,编译器会做基本检查。

主线程可以获得创建新线程时第三个参数的函数指针的返回值,通过pthread_join的第二个参数(输出型参数)

join无需等待异常,因为线程一旦异常全部退出,join等待无意义。异常是进程要处理的话题。

join拿到的第二个参数就是线程退出设定的返回值。

线程终止不能用exit(),exit()是终止进程的,正常来说线程终止时是入口函数return之后就终止。

线程终止接口pthread_exit().

还有一个:pthread_cancel

线程如果被取消,退出码是-1【PTHREAD_CANCELED】

线程分离

线程被创建出来默认状态就是需要被等待的【joinable】

如果线程被设置为分离,不用join,join会失败。

当然除了主线程分离新线程,新线程也可以主动去分离自己,pthread_detach(pthread_self())

详解pthread

pthread库需要映射到可执行文件中。

pthread库为管理每一个线程,把一个线程描述成struct_TCB().

当这个线程结束的时候,把返回值写入到TCB中的一个void* ret字段中,join时将ret写入到第二个参数中,以便获取到返回信息。

当一个线程被执行的时候,不仅会在库中创建管理块,还会再操作系统内核中创建轻量级进程(底层调用clone方法创建轻量级进程)(调用什么方法,栈在哪里),执行完对应方法之后把临时数据交给库中栈储存。

回答:

什么是线程id:

在pthread_creat的时候,线程在库中创建管理控制块时候,返回的空间起始虚拟地址。

线程传参和返回值:

线程在结束的时候将退出信息写入到控制块中的对应的(void*)的变量里。pthread_join来回收。

线程分离:

在线程控制块中有一个字段int joinable;默认情况下joinable=1,表示需要被等待回收;如果分离不回收,joinable被设置为0即可。库识别到线程退出,看到joinable=0,就会自动将空间释放。本质就是一个标志位。

linux中所有线程都在库中。进程创建出来的多个轻量级进程之间都是可以进行栈资源共享访问的,但是不能随意乱用。线程id就是进程地址空间上的一个虚拟地址,它指向的是进程动态库在pthread库内部,该线程对应的struct_pthread的起始地址。

线程局部存储:一个线程定义的一个变量不能被另一个线程获取改变,此时就用到线程局部存储。

类型前加上__thread。但是线程局部存储只能用来存储内置类型和部分指针,一些函数和类不行。

局部存储的应用:pthread_setname_np和pthread_getname_np;

将线程名字和线程id绑定,名字存储在线程的局部存储中,只有自己能访问!不会存在并发行为。

线程的互斥

既然线程之间共享很多资源,如果资源没有被保护起来,就会出现线程之间数据不一致问题。本小节主要讲解这一类问题。

进程什么时候被切走,三种情况:1.时间片到了 2.阻塞式IO 3.sleep(可以发现这些情况操作系统都处于内核态)进程什么时间点切新进程:当操作系统从内核态返回用户态的时候检查是否满足以上三种情况,如过满足就切换。

补充:一条汇编指令是原子性的(要么执行完要么不执行)

但是一条c/c++语句不是原子的,因为这一个语句可能被翻译成多条汇编指令,指令在被执行期间,进程可能被切走。

如何解决:

引入锁🔒 保护公共资源

PTHREAD_MUTEX_INITIALIZER是一个宏值,定义锁mutex.如果这样直接定义,那么这个锁无需手动释放。程序运行结束后自动被释放。

如果想要局部加锁,需要用到pthread_mutex_init()和pthread_mutex_destroy(),

一旦锁被定义出来不管是全局的还是局部的,都需要先申请锁.pthread_mutex_lock()

多个线程竞争锁,申请锁的这一步pthreead_mutex_lock()是原子的。解锁也是原子的。申请成功,继续执行临界区代码,申请失败就阻塞挂起申请执行流。

pthreead_mutex_trylock()是申请锁的非阻塞版本,暂时不考虑;

锁提供的功能本质上就是:执行临界区代码由并行转化成串行

加锁了所有线程都需要遵守

锁的原理

硬件级原理:关闭时钟中断(减少进程在执行临界资源中代码时被切换的风险)

软件级原理:内存中有一个mutex锁的变量假设是1,当一个进程申请到了锁,就会把mutex中的1和cpu相应的寄存器中存储的0进行交换(注意是交换不是拷贝,1只有一份,谁申请到了谁就持有锁),最后解锁就是重新将1写入到Mutex中。

在c++中,也可以直接使用锁;包含一下头文件include<mutex> ,定义锁std::mutex cpp_lock;

申请锁cpp_lock.lock(),解锁cpp_lock.unlock();

线程同步

总不能让一个线程占着共享的临界资源不放,所以为了解决不高效不公平的问题,有了线程同步。

首先一个线程不能释放锁的时候立即再次申请,释放完之后需要到等待队列末尾排队进行二次申请

条件变量

熟悉接口

生产和消费模型--原理

2个角色:生产者角色和消费者角色(由线程承担)

1个交易场所:以特定结构构成的一种内存空间。

3种关系:

生产者之间是互斥,竞争关系;

消费者之间是互斥关系。

生产者和消费者之间是互斥。同步关系

这种模型的好处:

1.生产过程和消费的过程是解耦的

2.高效

3.支持忙闲不均

好处并不体现在交易场所上,而是未来获取任务和处理任务上。这两步消耗的时间多。生产者需要做的只有两步:获取任务和生产任务,消费者同样两步:消费任务和处理任务。

编写基于blockqueue(阻塞队列)的生产消费模型

在多线程编程中阻塞队列是一种常用于实现生产者和消费者模型中的数据结构,其与普通的队列区别在于:当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入新的元素;同样,当队列满的时候,向队列里放入元素也会被阻塞,直到有元素从队列中取出。

阻塞队列是一个具有容量上限的队列,不满足读写条件时线程将会被阻塞。

POSIX信号量

首先信号量的本质就是一个计数器,是对特定资源的预定机制。

多线程使用资源有两种使用场景:

1.将目标资源整体使用

2.将目标资源按照不同的块,分批进行使用。

posix信号量接口

单生产单消费

多生产多消费的时候,是先申请信号量还是先申请锁?现申请信号量在申请锁的效率会高一些,因为先把资源划分完毕之后,那些申请资源失败的线程不会访问资源,只有成功的资源去竞争锁。

信号量适合用于资源被划分的一块块的场景下,在线程访问资源之前就判断好了(无需条件变量)

当资源是一整块的时候,就适合用阻塞队列。

相关推荐
myy-learn1 小时前
32 SQLITE数据库
jvm·数据库·sqlite
ComputerInBook1 小时前
linux包管理工具——yum(YellowdogUpdaterModified)
linux·运维·服务器·yum·dnf
霸道流氓气质1 小时前
通义千问 Chat 模型高级用法:Function Calling / Structured Output / 多轮对话
linux·运维·windows
en.en..1 小时前
Linux mmap 内存映射深度解析:基于帧缓冲 /dev/fb0
java·服务器·前端
实心儿儿1 小时前
Linux —— epoll(1)
linux·网络
s_w.h1 小时前
【 计网 】序列化与反序列化
linux·服务器·网络·算法·bash
Boop_wu2 小时前
[LangGraph] 案例 2 : 支持搜索的智能代理系统
服务器·windows·python·langchain
祖力552 小时前
网络编程:IO多路复用(select、poll、epoll)
linux·服务器·网络
数字智核2 小时前
2026昆山工厂空压机突然停机怎么办?找谁抢修
服务器·网络·数据库