朋友们、伙计们,我们又见面了,本期来给大家带来线程概念相关代码和知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
[1. 线程的概念](#1. 线程的概念)
[2. 线程的理解](#2. 线程的理解)
[3. 线程的优缺点](#3. 线程的优缺点)
[4. 重谈地址空间](#4. 重谈地址空间)
[4.1 虚拟地址的转化](#4.1 虚拟地址的转化)
[5.5 进程和线程](#5.5 进程和线程)
1. 线程的概念
关于线程有两种概念:
- ① 线程是比进程更加轻量化的一种执行流 / 线程是进程内部执行的一种执行流;
- ② 线程是CPU调度的基本单位 / 进程是承担系统资源的基本实体。
那么为什么要有线程呢?我们创建进程就是要让它执行我们的代码,但是进程的创建到运行这个阶段需要做很多事情(从磁盘加载可执行、由虚拟到物理的转化等等),如果我们只是想单纯的运行代码,那么有很多个可执行需要运行的话,通过创建进程的方式来运行就有点太麻烦了,所以需要有一种比进程更加轻量化的去执行我们的代码。
因为地址空间是进程的"资源窗口",所以进程可以这个窗口来进行资源的获取,所以为了减少成本,只创建PCB,然后指向同一个虚拟地址空间,实现共享,简单的理解就是将虚拟地址空间中的代码区、数据区、堆栈区进行划分为多个小块,此时多个PCB指向虚拟地址空间只参与资源的分配任务。那么既然有线程,OS也需要对这些线程进行管理,那么怎么管理呢?先描述、再组织,如果重新设置一套对于线程的管理方法那么实在太麻烦了,线程和进程很相似,所以直接采用管理进程的那一套方案即可,直接复用。
2. 线程的理解
CPU是调度执行流来运行的,那么在了解完线程之后,CPU获取到一个执行流大小是小于等于进程。这该怎么理解呢?我们如何看待现在的进程呢?内部包含多个执行流的进程!
进程 = 内核数据结构 + 代码和数据,现在所说的进程就是包含进程PCB、虚拟地址空间、页表、代码和数据。
其中不只包含一个执行流,而是有多个执行流(在一个地址空间有多个PCB)。
我们之前看待进程其中里面只有一个执行流,所以CPU在调度的时候,如果调度的进程只有一个执行流,那么就是进程,如果有多个执行流,那么就调度其中一个执行流,就叫做线程,所以CPU在调度执行流的大小总是小于等于进程!
所以线程是CPU调度的基本单位,而进程是承担系统资源的基本实体。
所以创建线程只需要创建PCB,所以线程也叫做轻量级进程。所以可以得出:
- 1. 线程的创建相比进程更加简单;
- 2. 线程在进程的地址空间中运行。
接下来我们通过代码的方式先直观的看一下线程,其中关于线程控制的代码会专门解释,先观察代码运行结果:
cpp#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
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!