🏠大家好,我是Yui_💬
🍑如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀
🚀如有不懂,可以随时向我提问,我会全力讲解~
🔥如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!
🔥你们的支持是我创作的动力!
🧸我相信现在的努力的艰辛,都是为以后的美好最好的见证!
🧸人的心态决定姿态!
💬欢迎讨论:如有疑问或见解,欢迎在评论区留言互动。
👍点赞、收藏与分享:如觉得这篇文章对您有帮助,请点赞、收藏并分享!
🚀分享给更多人:欢迎分享给更多对编程感兴趣的朋友,一起学习!
在前面的学习中,我们理解了什么是进程是:加载到内存中的程序也是内核数据结构+进程代码和数据还是资源分配的最小单位。
那么线程是什么,它和进程又有什么区别呢?
文章目录
1. 什么是线程
线程(Thread)是操作系统中的一个重要的执行单元,是程序执行的最小调度单元。线程存在于进程内部,一个进程可以包含一个或者多个线程,线程共享进程的资源并独立运行。
同时我们还要知道进程是承担系统资源分配的基本实体,而线程是CPU
运行的基本单位。
2. 深入理解线程
在学完进程后,我们都知道:
程序运行后,相关的代码和数据都会被加载到内存中,然后操作系统会为其创建相对应的
PCB
数据结构,生成虚拟地址空间、分配对应的资源,并通过页表建立映射关系。
以下是父子进程通过虚拟地址映射实际地址空间的逻辑图。
![[Pasted image 20240928145041.png]]
我们可以看到父子进程是相互独立的(进程就是相互独立的),哪怕是父子进程,它们也有各自的虚拟地址空间、映射关系、代码和数据这几样对象是必不可少的,也就是说,如果操作系统中没有线程只有进程的话,必然会存在大量的虚拟地址空间、映射关系、代码和数据。这样会导致操作系统的调度变的十分臃肿。
这是因为操作系统在调度进程时,需要频繁保存上下文数据、创建的虚拟地址空间和建立的映射关系
操作系统的设计者,在设计时为了避免这样的存在,引入了线程的概念。
线程拥有和进程类似的功能,但是线程在创建的时候只会额外创建一个task_struct
结构体,新创建的task_struct
也会指向当前的虚拟地址空间,且不需要额外建立映射关系和加载代码及数据。这也就造成了操作系统只需要针对task_struct
结构体即可完成调度,成本变低。
从这里我们也就可以发现,线程其实是进程的一部分,线程属于进程。
![[Pasted image 20250117153905.png]]
提问:为什么切换进程比切换线程开销大的多?
这就和计算机的硬件有关了。
我们知道CPU
内部包括:运算器、控制器、寄存器、MMU
、cache
,其中的cache
(高速缓存器),会遵循一个名为局部性原理
,会预先加载部分用户可能访问的数据以提高效率,如果切换进程,会导致高速缓存中的数据无效化,因为进程具有独立性,那么高速缓存器就会开始重新预加载,这是很浪费时间的;但是换作线程来说就不同了,因为线程是进程的一部分,共享数据,切换线程时所需要的数据不会改变,这也就意味着高速缓存中的数据可以继续使用,并可以接着预加载下一波数据。
那么现在就有了些新的概念了
进程的task_struct
称为PCB
,线程的task_struct
称为TCB
在前面的内容,我们知道了线程属于进程,那么我们现在无论对进程还是线程都可以称其为执行流,线程属于进程,当进程只有一个线程时,我们可以粗略的把当前进程当为一个单独的执行流;当进程中有多个线程时,则称当前进程为多执行流,其中每一个执行流就是一个个线程。
执行流的调度由操作系统负责,CPU只负责根据task_struct
结构进行计算。
- 如果下一个待调度的执行流为一个单独的进程,操作系统仍然需要创建
PCB
以及虚拟地址空间、建立映射关系、加载代码和数据。 - 但是如果下一个待调度的执行流为一个线程,操作系统只会创建一个
TCB
,并将其指向虚拟地址空间即可。
3. 进程与线程的关系
- 进程是资源分配的基本单位。
- 线程是调度的基本单位。
- 线程共享进程数据,但也拥有自己一部分数据。
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外各个线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式
- 当前的工作目录
- 用户id和组id
简单总结就是:
进程是由操作系统运行所需地址空间、映射关系、代码和数据打包后的资源,而线程/轻量级进程/执行流是利用进程资源完成任务的基本单位。
![[Pasted image 20250117162248.png]]
具体比对还可见以下表格:
特性 | 线程 | 进程 |
---|---|---|
单位 | 执行的最小调度单位 | 资源分配的最小单位 |
资源共享 | 同一进程的线程共享资源 | 进程间资源隔离 |
开销 | 创建和切换开销较小 | 创建和切换开销较大 |
通信 | 同进程线程通信简单 | 需要使用 IPC(管道、共享内存等) |
崩溃影响 | 一个线程崩溃会影响进程 | 一个进程崩溃对其他进程无直接影响 |
3.1 Linux与Windows不同的线程设计
在Linux中,由于PCB
和TCB
的共同点太多了,于是直接复用了PCB
的设计和调度策略,这样大大减少了系统的调度时的开销,因此Linux中实际没有真正的线程概念,有的只是复用了PCB
思想的TCB
。
在这种设计思想下,线程注定不会过于庞大,因此Linux中的线程又可以称为轻量级进程LWP
,轻量级进程足够简单,且易于维护,效率更高、安全性强,可以使得Linux系统不间断的运行,不容易崩溃。
而Windows使用的是真线程方案,Windows为线程重新设计了一套逻辑,这也就导致了操作系统在同时面临PCB
和TCB
时需要进行识别,转化不同的处理方法。这种处理方法,导致了系统运行的复杂化,也就人系统运行变得不稳定,这也就导致了Windows系统无法长时间运行,需要通过重新启动来重置风险。
4. 简单使用线程
4.1 pthread_create函数
pthread_create
函数是POSIX标准中用于创建新线程的函数,它运行在同一进程中并发执行多个任务。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);
参数说明:
pthread_t* thread
:- 用于存储创建的线程ID(线程句柄)。
- 线程ID在后续操作(如
pthread_join
)中用于标识线程。
const pthread_attr_t* attr
:- 线程的属性。可以设置为
NULL
使用默认属性。 - 自定义属性可以用于指定线程栈大小、调度策略等。
- 线程的属性。可以设置为
void* (*start_routine)(void*)
:- 线程执行的函数指针。
- 函数的返回值可以通过
pthread_join
获取。
void* arg
:- 传递给函数的参数,如果线程函数需要多个参数,可以将参数打包为一个结构体后传递。
返回值:
- 传递给函数的参数,如果线程函数需要多个参数,可以将参数打包为一个结构体后传递。
0
:表示线程创建成功。- 非0:表示线程创建失败,返回错误代码。
了解完后开始实操
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <pthread.h>
void* rout(void* arg){
while(true){
std::cout<<"i am a thread\n";
sleep(1);
}
}
int main()
{
pthread_t tid;
if(pthread_create(&tid,nullptr,rout,NULL)!=0){
perror("pthread_create");
exit(1);
}
while(true){
std::cout<<"i am a main\n";
sleep(1);
}
return 0;
}
注意编译时,需要加上-lpthread
指明线程原生库,具体看下图输入。
运行结果:
![[Pasted image 20250117173900.png]]
可以看到,两个死循环同时运行。
可以使用指令来查看当前系统中的线程信息。
ps -aL | head -1 && ps -aL | grep a.out | grep -v grep
![[Pasted image 20250117174235.png]]
可以看到有两个线程。它们的PID
都是相同的,但是LWP
不同,且第一个线段的LWP
和PID
相同。
直接说结论:第一个线程是主线程,也就是之前的进程,它们的PID
和LWP
都是相同的。
提问:操作系统如何判断调度是,是切换线程还是切换进程?
- 将待切换的执行流
PID
与当前执行流的PID
进行对比,如果相同,说明接下来要切换的是线程,反之就是进程。
线程是进程的一部分,给其中任何一个线程发送信号,都会影响到其他线程,进而影响到整个进程。
5.线程的优缺点
5.1 线程的优点
- 创建一个新线程的代价要比创建一个新进程要小的多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要小的多。
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
5.2 线程的缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
6. 总结
线程在现代计算中至关重要,合理使用线程可以显著提高程序的性能和响应速度,但也需要注意同步和调试的复杂性。