朋友们、伙计们,我们又见面了,本期来给大家带来线程概念相关代码和知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
编辑
1. 线程的概念
2. 线程的理解
3. 线程的优缺点
4. 重谈地址空间
4.1 虚拟地址的转化
5.5 进程和线程
1. 线程的概念
关于线程有两种概念:
- ① 线程是比进程更加轻量化的一种执行流 / 线程是进程内部执行的一种执行流;
- ② 线程是CPU调度的基本单位 / 进程是承担系统资源的基本实体。
那么为什么要有线程呢?
我们创建进程就是要让它执行我们的代码,但是进程的创建到运行这个阶段需要做很多事情(从磁盘加载可执行、由虚拟到物理的转化等等),如果我们只是想单纯的运行代码,那么有很多个可执行需要运行的话,通过创建进程的方式来运行就有点太麻烦了,所以需要有一种比进程更加轻量化的去执行我们的代码。
因为地址空间是进程的“资源窗口”,所以进程可以这个窗口来进行资源的获取,所以为了减少成本,只创建PCB,然后指向同一个虚拟地址空间,实现共享,简单的理解就是将虚拟地址空间中的代码区、数据区、堆栈区进行划分为多个小块,此时多个PCB指向虚拟地址空间只参与资源的分配任务。
那么既然有线程,OS也需要对这些线程进行管理,那么怎么管理呢?先描述、再组织,如果重新设置一套对于线程的管理方法那么实在太麻烦了,线程和进程很相似,所以直接采用管理进程的那一套方案即可,直接复用。
2. 线程的理解
CPU是调度执行流来运行的,那么在了解完线程之后,CPU获取到一个执行流大小是小于等于进程。这该怎么理解呢?我们如何看待现在的进程呢?内部包含多个执行流的进程!
进程 = 内核数据结构 + 代码和数据,现在所说的进程就是包含进程PCB、虚拟地址空间、页表、代码和数据。
其中不只包含一个执行流,而是有多个执行流(在一个地址空间有多个PCB)。
我们之前看待进程其中里面只有一个执行流,所以CPU在调度的时候,如果调度的进程只有一个执行流,那么就是进程,如果有多个执行流,那么就调度其中一个执行流,就叫做线程,所以CPU在调度执行流的大小总是小于等于进程!
所以线程是CPU调度的基本单位,而进程是承担系统资源的基本实体。
所以创建线程只需要创建PCB,所以线程也叫做轻量级进程。
所以可以得出:
- 1. 线程的创建相比进程更加简单;
- 2. 线程在进程的地址空间中运行。
接下来我们通过代码的方式先直观的看一下线程,其中关于线程控制的代码会专门解释,先观察代码运行结果:
#include <iostream> #include <unistd.h> #include <pthread.h> #include <sys/types.h> #include <unistd.h> // 新线程 void *ThreadRoutine(void *arg) { const char *threadname = (const char *)arg; while (true) { std::cout << "I am a new thread: " << threadname << ", pid: " << getpid() << std::endl; sleep(1); } } int main() { pthread_t tid; // 创建线程 pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1"); // 主线程 while (true) { std::cout << "I am main thread" << ", pid: " << getpid() << std::endl; sleep(1); } return 0; }
这段代码首先会创建一个新的线程,然后一个执行流去执行新线程的代码,紧接着往下走就是主线程的代码,代码结果就是会有两个循环在一直执行:
使用指令 ps -aL可以查看线程:
可以看出线程的调度区分并不是用PID来区分,而是LWP,并且主线程的LWP和PID一样。
3. 线程的优缺点
优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点:
- 健壮性降低,多线程在运行时,各个线程之间不相互独立,一个线程的异常很有可能导致其它线程出现异常,换句话说线程之间是缺乏保护的
- 缺乏访问控制,在一个线程中调用某些OS函数有可能会对整个进程造成影响
- 调试难度比较高,多线程程序比单线程程序调试更为复杂
4. 重谈地址空间
我们的可执行程序被编译好之后以ELF的格式存储在磁盘上,当要执行时会先加载的内存,在之前文件系统部分了解到文件系统IO的基本单位大小是4KB,即使我们所要修改的数据只有1比特位,也需要一次性加载4KB,所以为了统一和方便,将物理内存以4KB划分为多个小块,每一个块被叫做一个页框,磁盘文件中的文件以4KB分为多个小块,每一块被叫做一个页帧,这样子每一次加载磁盘文件时,就根据4KB很容易的将数据加载进来。
以32位4GB的机器来计算,页框的数量有10万多个页框,所以OS也需要将这些页框给管理起来,所以就需要先描述再组织,为页框构建描述结构体的对象(包含页框的属性、使用情况、标志位等等),然后用数组将这些结构体管理,对页框的管理就变成了对数组的增删查改。
4.1 虚拟地址的转化
通过上面对物理内存以4KB进行划分,所以虚拟地址到物理地址的转化不只只通过页表来转化,虚拟到物理的转化其实是在CPU内部转化的,在我们的CPU中的eip寄存器保存的是虚拟地址,当CPU拿到一个虚拟地址时会通过MMU从虚拟到物理的转化:
例如:虚拟地址32个比特位
1111 1111 1110 0000 0000 0000 0000 0001
- 前10个比特位可以找到对应的页目录;
- 次10个比特位可以找到页表,通过页表可以找到页框;
- 页框起始地址 + 低12个比特位可以找到页内偏移;
通过整个的虚拟地址就可以访问到物理地址上的数据内容。
可以看到其实划分页表的本质就是划分地址空间。
5.5 进程和线程
进程是资源分配的基本单位线程是调度的基本单位线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
简单的每个线程都有自己独立的硬件上下文数据与栈结构。
各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!