这次,我们来讲解关于线程的部分知识点!
关于线程部分,我们接下来的
流程大概:
1.Linux中线程该如何理解?
2.重新定义线程和进程
3.第四次谈进程地址空间
4.Linux中线程的周边概念
现在,我们正式进入线程知识:
正文
什么是线程?
线程是"一个进程内部的控制序列。
简单来说,在一个程序里的一个执行路线就叫做线程(thread)
现在,我们通过画图的形式帮助我们理解它:

线程是进程内的一个执行分支。线程的执行力度,要比进程要细。
Linux中,线程的实现方案:
1.线程在进程"内部"执行,线程在进程的地址空间内运行,为什么?
因为,任何执行流要执行,都要有资源!而进程地址空间是进程的资源窗口!
2.线程的执行粒度要比进程要更细?因为线程只是执行进程代码的一部分。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
那么,我们回头想想,之前我们的进程:
如何理解我们以前的进程?
操作系统以进程为单位,给我们分配资源,我们当前的进程内部,只有一个执行流!
完善版本:一大堆的执行流,地址空间,页表,该进程在物理内存中保存的数据(所申请的代码数据里的内存空间)
即:进程是承担分配系统资源资源的基本实体。
重新定义线程和进程:
什么叫做线程?我们认为,线程是操作系统调度的基本单位。
之前,我们讲到:
进程=内核数据结构(task_struct)+代码和数据
现在重新理解进程:
内核观点:
进程是承担分配系统资源的基本实体!
线程的优点:
1.创建一个新线程的代价要比创建一个新进程小得多
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3.线程占用的资源要比进程少很多
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点:
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程用途:
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现
第四次重谈地址空间


内存管理的基本单位:页--4kb
在现代操作系统中,为了高效管理内存,会将物理内存和进程的虚拟内存均划分为大小固定的块,这种块就称为"页"。通过以"页"为单位进行内存分配、回收和地址映射(虚拟地址到物理地址),可以显著减少内存碎片,提升内存利用效率和管理速度。
这个页的大小一般是4kb.
因为:页表示映射,最终只能帮我们找到对应的页框,C语言中,任何类型取地址只有一个。
这也解释了语言中取地址,都是它众多开辟的地址值是最小的。
因为:它是 起始地址+偏移量 -----来读取内存块
同时,在内存中,没有类型的概念,类型是给CPU去看的,即CPU读取时,本身是知道读取多少字节的!
CPU在硬件上是直接与物理内存连着的,通过软件啊的方式帮我们定位到它的起始地址,接下来CPU不是来读取我们的物理内存吗?读的过程就是把物理内存在硬件上拷贝给CPU,CPU自己也知道拷多少。
任何进程都要有页目录,从而实现虚拟地址到物理地址的高效转换!
CR2寄存器与CR3寄存器的区别:
CR2:存储触发页面错误的虚拟地址(遇到页面缺失,权限不符等错误)。当操作系统的页面错误处理程序可读取CR2的值,定位到出错地址,进而决定是加载缺失页面到内存,还是终止违规进程。
CR3:存储当前进程页目录表的物理基地址,还包含页缓存禁止,写入策略等标志位。当进程切换的时候,OS会自动更新CR3的值,使其指向新进程的页目录表,从而实现进程地址空间隔离;同时修改CR3还会触发TLB(转换查找缓存区)刷新,保障地址转换到准确性!
线程在执行,本质上是进程在执行,因为线程是进程的执行分支。
指令:
查看CPU硬件信息的虚拟文件 cat /proc/cpuinfo
Linux的周边概念:
为什么说线程比进程要更轻量化?
1.创建和释放更加轻量化(生死)
2.切换更加轻量化(运行)
因此,在整个生命周期也就比进程更轻量化了!
那怎么理解上面的两点呢?
第一点很好理解:
因为通过上面我们知道线程其实就复用了进程的数据结构与管理算法!
相比进程,它只需要管理控制块就行,而进程不仅管理控制块,还有地址空间、页表、物理内存的一部分代码!
第二点理解:我们知道,线程是CPU调度的基本单位,但并不代表进程就不需要调度,因为OS也要调度进程。
其实,在CPU当中,有一个cache缓存组件,它用于提升数据访问效率的高速存储组件。作为CPU与内存之间的高速缓冲层,速度远快于内存但容量较小,用于临时存储CPU近期频繁访问的数据、指令,减少CPU等待内存数据的时间。
它的工作原理:
基于"局部性原理"(CPU近期访问的数据,短时间内会被重复访问):
- 当CPU读取数据时,先查cache:若存在("缓存命中"),直接从cache读取;若不存在("缓存未命中"),则从内存加载数据到cache,再供CPU读取。
我们通常把被频繁访问、使用频率高的数据称为热数据!
在线程之间的切换,由于线程同属一个进程、共享地址空间,线程切换后Cache中仍保留该进程的热数据,因此无需重新加载,Cache命中率高,
而进程具有独立地址空间,进程切换后新进程需重新在Cache中加载自己的热数据,初期Cache命中率低,切换开销更大。
因此,就有了在运行当中,线程切换更加轻量化了!
我们在上面讲到:
1.内核无"线程"概念,仅支持轻量级进程
内核层面不直接提供"线程"的系统调用,仅提供轻量级进程(LWP)的系统调用。
2. 用户态线程由pthread库封装实现用户需要的线程接口,由 pthread线程库 在应用层对"轻量级进程接口"进行封装后提供,实现了用户态的线程抽象。
3. pthread库的特性
几乎所有Linux平台默认自带该库;
Linux中编写多线程代码,需依赖pthread库(属于第三方库)。
了解线程接口:


创建成功返回0,失败返回-1.
我们来编写一下代码:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
void*mythread(void*args)
{
while(true)
{
printf("hello ,i am pthread\n");
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,mythread,0);
while(true)
{
printf("hello,i am main thread\n");
sleep(1);
}
return 0;
}
注意:

我们会上个有个链接错误,这是因为它找不到pthread这个库头文件,听到这里,不知道有没有感到很熟悉,我们在动静态库链接时,也出现过这种问题,所以,我们要知道库的头文件在哪里!


所以


上面动图,我们就很清楚看到,它们是并行运行的!
我们也可以推测出:

查看线程ID
指令
ps -aLLWP:light weight process 轻量化进程
线程等待:


#include<iostream>
#include<pthread.h>
#include<unistd.h>
void*mythread(void*args)
{
int cnt=0;
while(cnt<10)
{
printf("hello ,i am pthread\n");
cnt++;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,mythread,0);
int cnt=0;
while(cnt<5)
{
printf("hello,i am main thread\n");
cnt++;
sleep(1);
}
void*retval;
pthread_join(tid,&retval);
std::cout<<"main thread quit,retval:"<<retval<<std::endl;
return 0;
}

线程异常情况:
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
void *mythread(void *args) { while (true) { std::cout << "i am mythread" << std::endl; sleep(5); int a = 10; a /= 0; } return nullptr; } int main() { pthread_t tid; pthread_create(&tid, nullptr, mythread, 0); while (true) { std::cout << "i am main pthread" << std::endl; sleep(1); } pthread_join(tid,nullptr); return 0; }
线程的概念是库给我们维护的,线程库要维护线程的概念,不用维护线程的执行流!那么,要维护线程的概念,注定了维护多个线程属性的集合,线程库要不要管理起来这些线程呢?
要!怎么管理?先描述,再组织!
此外,我们用到原生线程库,要不要加载到内存中呢?加载到哪里?答案是:要的!原生线程库的本质是:代码(函数)+数据(tcb、线程栈)的集合,又由于冯诺依曼体系结构规定,它们都是基于内存中的!
我们上面讲到过线程是共享的!所以线程必定是在共享区的,那我们现在来看看线程在共享区的内核结构是怎么样的!

线程除了共享进程数据,也拥有自己的一部分数据:
1.线程ID
2.一组寄存器
3.栈
4.errno
5.信号屏蔽字
6.调度优先级
线程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,
如果定义一个函数,在各线程中都可以调用,
如果定义一个全局变量,在各线程中都可以访问到,
除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录用户id和组id
好了,关于本次线程的部分知识就到处结束了,希望大家一起进步!
最后,到了本次鸡汤环节:
厉害的人都在做 "难而正确的事"。





