目录
一、前言
二、 什么是线程
💧线程的引入💧
💧线程的基本概念 💧
💧线程的理解 💧
💧进程与线程的关系💧
💧程序如何划分(重拾页表、见一下LWP)💧
三、简单的使用线程
四、线程小结
🔥再谈线程🔥
🔥线程的优点 🔥
🔥线程的缺点🔥
🔥线程和进程之间的区别🔥
🔥线程的用途 🔥
五、共勉
一、前言
将一份【代码成功编译】后,可以得到一个【可执行程序】,程序运行后,相关代码和数据被 【load】 到内存中,并且操作系统会生成对应数据结构(比如 PCB)对其进行管理及分配资源,准备工作做完之后,我们就可以得到一个运行中的程序,简称为 进程,对于操作系统来说,只有 进程 的概念是无法满足高效运行的需求的,因此需要一种执行粒度更细、调度成本更低的执行流,而这就是 线程
Windows
中的线程
二、 什么是线程
【线程】是操作系统管理任务执行(CPU)的基本单位,它允许一个程序同时执行多个任务(进程中有很多执行流),从而实现并发(在同一时间段内处理多个任务)或并行(同时处理多个任务)。
💧线程的引入💧
想要真正的了解【线程】,就需要先了解【进程】因为线程是从进程中延申出来的,由于它们之间的概念十分的枯燥难以理解,我们用一个流程图,来给大家清楚的说明,线程和进程之间到底有那些千丝万缕的联系。
-
CPU与进程:CPU比作工厂,它一次只能处理一个车间(进程)的任务。如果一个车间正在使用电力,其他车间(进程)就必须等待。
-
进程:进程就好比是工厂中的车间,代表CPU正在执行的任务。在任一时刻,CPU只能运行一个进程,而其他进程则处于非运行状态。
-
线程:线程比作车间里的工人。一个车间(进程)可以有很多工人(线程),他们协同工作以完成一个任务。一个进程可以包含多个线程。
-
共享内存空间:车间的空间是工人们共享的,这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
-
互斥锁(Mutex):为了防止多个线程同时读写某一块内存区域,可以使用互斥锁。这就像是一个房间里面有人的时候,其他人就不能进去,必须在门口排队等待。
-
资源限制:有些房间(内存区域或资源)最多只能容纳一个人,这代表某些资源一次只能被一个线程使用。
看完这幅图和解析,我相信大家已经能够对 进程和线程有了一个基本的认识,下面我们再来深度的解析,线程
💧线程的基本概念 💧
【教材观点】
- 【线程】就是【进程】的一个执行分支、执行粒度比进程更细、调度成本更低
- 线程就是进程内部的一个执行流
【内核观点】
- 【进程】是 承担系统资源分配的基本实体,而 【线程】是
CPU
运行的基本单位
💧线程的理解 💧
【线程】是对以往【进程】概念的补充完善,正确理解线程概念是一件十分重要的事,理解 线程 之前需要先简单回顾一下 【进程】
- 程序运行后,相关的代码和数据会被
load
到内存中,然后操作系统为其创建对应的PCB
数据结构、生成虚拟地址空间、分配对应的资源,并通过页表建立映射关系
详见:《Linux【进程地址空间】》
进程之间是相互独立
- 即使是 父子进程,他们也有各自的 虚拟地址空间、映射关系、代码和数据(可能共享部分数据,出现修改行为时引发 写时拷贝机制)
- 如果我们想要创建 其他进程 执行任务,那么 虚拟地址空间、映射关系、代码和数据 这几样东西是必不可少的,想象一下:如果只有进程的概念,并且同时存在几百个进程,那么操作系统调度就会变得十分臃肿
- 操作系统在调度进程时,需要频繁保存上下文数据、创建的虚拟地址空间及建立映射关系
线程的出现
- 为了避免这种繁琐的操作,引入了 【线程】 的概念,所谓 【线程】 就是:额外创建一个 task_struct 结构,该 task_struct 同样指向当前的虚拟地址空间,并且不需要建立映射关系及加载代码和数据,如此一来,操作系统只需要 针对一个 task_struct 结构即可完成调度,成本非常低
为什么【切换进程】比 【切换线程】开销大得多?
- 在
CPU
内部包括:运算器、控制器、寄存器、MMU
、硬件级缓存(cache
),其中 硬件级缓存cache
又称为 高速缓存,遵循计算机设计的基本原则:局部性原理,会预先加载 部分用户可能访问的数据 以提高效率。 - 如果【切换进程】,会导致 高速缓存 中的数据无法使用(进程具有独立性),重新开始 预加载,这是非常浪费时间的(对于
CPU
来说)。 - 但【切换线程】就不一样了,因此线程从属于进程,切换线程时,所需要的数据的不会发生改变,这就意味值 高数缓存 中的数据可以继续使用,并且可以接着 预加载 下一波数据
进程(
process
)的task_struct
称为PCB
,线程(thread
)的task_struct
则称为TCB
- 从今天开始,无论是 【进程】 还是 【线程】,都可以称为 执行流,线程 从属于 进程:当进程中只有一个线程时,我们可以粗粒度的称当前进程为一个单独的执行流;当进程中有多个线程时,则称当前进程为多执行流,其中每一个执行流就是一个个的线程
总结:执行流的调度由操作系统负责,
CPU
只负责根据task_struct
结构进行计算
- 若下一个待调度的执行流为一个单独的进程,操作系统仍需创建
PCB
及 虚拟地址空间、建立映射关系、加载代码和数据 - 但如果下一个待调度的执行流为一个线程,操作系统只需要创建一个
TCB
,并将其指向已有的虚拟地址空间即可
现在面临着一个很关键的问题:进程和线程究竟是什么关系?
💧进程与线程的关系💧
【进程】是 承担系统资源分配的实体,比如 程序运行必备的:虚拟地址空间、页表映射关系、相关数据和代码 这些都是存储在 进程 中的,也就是我们历史学习中 进程 的基本概念
【线程】是 CPU 运行的基本单位,程序运行时,CPU 只认 task_struct 结构,并不关心你是 【线程】 还是 【进程】,不过,线程 包含于 进程 中,一个 进程 可以只有一个 线程,也可以有很多 线程,当只有一个 线程 时,通常将其称为 进程,但对于 CPU 来说,这个 进程 本质上仍然是 线程;因为 CPU 只认 task_struct 结构,并且 PCB 与 TCB 都属于 task_strcut,所以才说 线程是 CPU 运行的基本单位
总结:
- 【进程】是由操作系统将程序运行所需地址空间、映射关系、代码和数据打包后的资源包
- 【线程/轻量级进程/执行流 】则是利用资源完成任务的基本单位
线程包含于进程中,进程本身也是一个线程
我们之前学习的进程概念是不完整的,引入线程之后,可以对进程有一个更加全面的认识
- 通常将程序启动,比如
main
函数中的这个线程称为 主线程,其他线程则称为 次线程
实际上 进程 =
PCB
+TCB
+ 虚拟地址空间 + 映射关系 + 代码和数据,这才是一个完整的概念以后谈及进程时,就要想到 一批执行流+可支配的资源
【进程】与【线程】的概念并不冲突,而是相互成就
- 在 Linux 中,认为 【PCB】 与 【TCB】 的共同点太多了,于是直接复用了 PCB 的设计思想和调度策略,在进行 【线程管理】 时,完全可以复用 【进程管理】 的解决方案(代码和结构),这可以大大减少系统调度时的开销,做到 小而美,因此 Linux 中实际是没有真正的 线程 概念的,有的只是复用 PCB 设计思想的 TCB
- 在这种设计思想下,【线程】 注定不会过于庞大,因此
Linux
中的 线程 又可以称为 轻量级进程(LWP
),轻量级进程 足够简单,且 易于维护、效率更高、安全性更强,可以使得Linux
系统不间断的运行程序,不会轻易 崩溃
在Linux系统中,所有的执行流都被称为轻量级进程(Lightweight Process,LWP),实际上就是操作系统概念中的线程。在Linux中,线程和进程的区别并不是很明显,因为Linux将线程实现为与进程相似的实体,即轻量级进程。
- 在Linux中,每个【轻量级进程】(线程)都对应一个task_struct结构体,操作系统通过调度算法选择下一个要执行的轻量级进程,而不关心这个task_struct属于哪个进程,或者是属于一个进程的其中一个线程。
- 因此,在Linux中,CPU调度的实际执行单元是轻量级进程(线程),而不是进程。每个轻量级进程都有自己的执行流,可以独立执行代码,拥有独立的栈空间和寄存器状态。多个轻量级进程可以共享一个进程的资源,实现并发执行。
💧程序如何划分(重拾页表、见一下LWP)💧
操作系统进行内存管理的基本单位是【4KB】
内存里都是以【4kb】大小分的一个一个内存块——空间
可执行程序也是以【4kb】进行分——内容
现在我们再来重新看待页表:
- 后12位叫做叶内偏移
- 同时读取时还要结合数据类型的大小
总结:多个执行流即不同的线程执行不同的代码,获得各自的数据,本质就是让不同的线程各自看到不同的页表。
三、简单的使用线程
如何验证
Linux
中的【线程】呢? 简单使用一下就好了接下来简单使用一下
pthread
线程原生库中的线程相关函数(只是简单使用,不涉及其他操作)
- 给不同的线程分配不同的区域,本质就是给让不同的线程,各自看到全部页表的子集
#include <iostream>
#include <thread>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
// 线程分支
void *test(void *arg)
{
while (true)
{
// 在线程中打印进程PID
std::cout << "I am new thread, pid: " << getpid() << std::endl;
sleep(1);
}
}
int main()
{
// 线程id
pthread_t tid;
pthread_create(&tid, nullptr, test, nullptr); //创建一个新线程
while (true)
{
// 打印主线程的 进程PID
std::cout << "I am main thread, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
- 函数编译完成,会出现两条分支,一条是主线程,一条之分支线程,我们需要观察吗,这两个线程是否在同一个进程内,这两个线程会有区别吗?
- 编译程序时,需要带上
-lpthread
指明使用 线程原生库 - 结果:主线程+一个次线程同时在运行
使用指令查看当前系统中正在运行的 线程 信息
while :; do ps -aL | head -1 && ps -aL | grep thread ; echo "-----------------------------------"; sleep 1 ; done
可以看到此时有 两个个线程
- 细节1:两个个线程的
PID
都是 4071708 - 细节2:两个线程的
LWP
各不相同 - 细节3:第一个线程的
PID
和LWP
是一样的
其中,第一个线程就是 主线程,也就是我们之前一直很熟悉的 进程,因为它的 PID
和 LWP
是一样的,所以只需要关心 PID
也行
操作系统如何判断调度时,是切换为 线程 还是切换为 进程 ?
- 将待切换的执行流
PID
与当前执行流的PID
进行比对,如果相同,说明接下来要切换的是 线程,否则切换的就是 进程 - 操作系统只需要找到
LWP
与PID
相同的线程,即可轻松锁定主线程
线程是进程的一部分,给其中任何一个线程发送信号,都会影响到其他线程,进而影响到整个进程
四、线程小结
🔥再谈线程🔥
Linux
中没有 真线程,有的只是复刻 进程 代码和管理逻辑的 轻量级线程(LWP
)
线程 有以下概念:
- 在一个程序中的一个执行路线就叫做 线程(Thread),或者说 线程 是一个进程内部的控制程序
- 每一个进程都至少包含一个 主线程
- 线程 在进程内部执行,本质上仍然是在进程地址空间内运行
- 在 Linux 系统中,CPU 看到的 线程 TCB 比传统的 进程 PCB 更加轻量化
- 透过进程地址空间,可以看到进程的大部分资源,将资源合理分配给每个执行流,就形成了 线程执行流
🔥线程的优点 🔥
线程 最大的优点就是 轻巧、灵活,更容易进行调度
- 创建一个线程的代价比创建一个进程的代价要小得多
- 调度线程比调度进程要容易得多
- 线程占用的系统资源远小于进程
- 可以充分利用多处理器的并行数量(进程也可以)
- 在等待慢速 IO 操作时,程序可以执行其他任务(比如看剧软件中的 “边下边看” 功能)
- 对于计算密集型应用,可以将计算分解到多个线程中实现(比如 压缩/解压 时涉及大量计算)
- 对于 IO密集型应用,为了提高性能,将 IO操作重叠,线程可以同时等待资源,进行 高效IO(比如 文件/网络 的大量 IO 需要,可以通过 多路转接 技术,提高效率)
线程 的合理使用可以提高效率,但 线程 不是越多越好,而是 合适 最好,让每一个线程都能参与到计算中
🔥线程的缺点🔥
线程 也是有缺点的:
- 性能损失,当 线程 数量过多时,频繁的 线程 调度所造成的消耗会导致 计算密集型应用 无法专心计算,从而造成性能损失
- 健壮性降低,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的
- 缺乏访问控制,进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
🔥线程和进程之间的区别🔥
虽然进程和线程之间很相似,但是它们之间还是存在着一些细小的区别
- 进程:是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的实例。每个进程都有自己的独立内存空间。
- 线程:是进程中的执行单元,多个线程共享同一进程的内存空间和资源,但每个线程有自己的栈、程序计数器等。线程是CPU调度的基本单位。
内存空间:
- 进程:每个进程都有自己独立的内存空间,包括代码段、数据段、堆和栈。进程之间的内存是隔离的,不能直接访问对方的内存空间。
- 线程:线程共享进程的内存空间,因此可以直接访问同一进程中的其他线程的数据,但这也带来了同步和并发控制的复杂性。
资源开销
- 进程:创建和切换进程的开销较大,因为涉及到完整的内存空间、资源的分配与回收。
- 线程:线程创建和切换的开销相对较小,因为线程共享进程的资源和内存,切换时不需要进行完整的资源分配和回收。
通信方式
- 进程:进程间通信(IPC)需要通过操作系统提供的机制,如管道、消息队列、共享内存、信号量等,通信相对复杂且效率较低。
- 线程:线程之间可以通过共享内存直接通信,通信效率较高,但需要小心处理同步问题,以避免竞态条件和死锁。
并发性
- 进程:由于进程是独立的单元,一个进程的崩溃通常不会影响其他进程。但进程间的并发性较低。
- 线程:线程之间的并发性较高,可以在同一进程内并行执行任务。然而,一个线程的崩溃可能导致整个进程的崩溃。
进程与线程的一个简陋模型用图表示的话,是这样的:
🔥线程的用途 🔥
合理的使用 多线程,可以提高
CPU
计算密集型程序的效率
- 进程:适合用于多任务、多用户的独立应用,如操作系统中的服务程序、独立的应用程序等。
- 线程:适合用于需要并行执行、共享大量数据的任务,如服务器的多线程处理、实时图像处理等。
五、共勉
以下就是我对【Linux系统编程】线程的深度解析 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新【Linux系统编程】,请持续关注我哦!!!