在前面的学习中,我们理解了什么是进程是:加载到内存中的程序也是内核数据结构+进程代码和数据还是资源分配的最小单位。 那么线程是什么,它和进程又有什么区别呢?
线程(Thread)是操作系统中的一个重要的执行单元,是程序执行的最小调度单元。线程存在于进程内部,一个进程可以包含一个或者多个线程,线程共享进程的资源并独立运行。
同时我们还要知道进程是承担系统资源分配的基本实体,而线程是CPU
运行的基本单位。
在学完进程后,我们都知道:
程序运行后,相关的代码和数据都会被加载到内存中,然后操作系统会为其创建相对应的
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
,并将其指向虚拟地址空间即可。进程是由操作系统运行所需地址空间、映射关系、代码和数据打包后的资源,而线程/轻量级进程/执行流是利用进程资源完成任务的基本单位。
![[Pasted image 20250117162248.png]]
具体比对还可见以下表格:
特性 | 线程 | 进程 |
---|---|---|
单位 | 执行的最小调度单位 | 资源分配的最小单位 |
资源共享 | 同一进程的线程共享资源 | 进程间资源隔离 |
开销 | 创建和切换开销较小 | 创建和切换开销较大 |
通信 | 同进程线程通信简单 | 需要使用 IPC(管道、共享内存等) |
崩溃影响 | 一个线程崩溃会影响进程 | 一个进程崩溃对其他进程无直接影响 |
在Linux中,由于PCB
和TCB
的共同点太多了,于是直接复用了PCB
的设计和调度策略,这样大大减少了系统的调度时的开销,因此Linux中实际没有真正的线程概念,有的只是复用了PCB
思想的TCB
。
在这种设计思想下,线程注定不会过于庞大,因此Linux中的线程又可以称为轻量级进程LWP
,轻量级进程足够简单,且易于维护,效率更高、安全性强,可以使得Linux系统不间断的运行,不容易崩溃。
而Windows使用的是真线程方案,Windows为线程重新设计了一套逻辑,这也就导致了操作系统在同时面临PCB
和TCB
时需要进行识别,转化不同的处理方法。这种处理方法,导致了系统运行的复杂化,也就人系统运行变得不稳定,这也就导致了Windows系统无法长时间运行,需要通过重新启动来重置风险。
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
: pthread_join
)中用于标识线程。const pthread_attr_t* attr
: NULL
使用默认属性。void* (*start_routine)(void*)
: pthread_join
获取。void* arg
: 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
进行对比,如果相同,说明接下来要切换的是线程,反之就是进程。
线程是进程的一部分,给其中任何一个线程发送信号,都会影响到其他线程,进而影响到整个进程。线程在现代计算中至关重要,合理使用线程可以显著提高程序的性能和响应速度,但也需要注意同步和调试的复杂性。