目录
一,线程的基本概念
- 线程指的是进程中一个单一顺序的控制流(执行流),属于进程的一部分
- 一个进程中可以并发多个线程,每条线程并行执行不同的任务(进程:线程 = 1:n)
- 线程是独立调度和分派的基本单位,而进程是分配资源的基本单位
- 同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的pcb,调用栈,自己的寄存器环境(上下文),自己的线程本地存储
总结:
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,比进程更细更轻量化(其实CPU调度的都是轻量化进程)。线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源(自己的调用栈,寄存器环境等),但线程可以共享进程所拥有的全部资源,同时线程也拥有各自私有资源(不能被其他线程共享)
创建 task_struct,且创建出来的 task_struct 和原来的 task_struct 共享进程地址空间和页表,事实上,我们所创建的其实就是三个线程,只不过这三个线程共用一张 mm_struct 和 一张页表,这三个 task_struct 就是三个不同的执行流;
总结:
在Linux中创建线程,只需创建相应的 PCB 即可,所以在Linux中,线程是在进程的内部 “运行”,线程在该进程的地址空间内 “运行”,拥有该进程的一部分资源
二,如何重新看待进程
进程 = 内核数据结构 + 进程对应的代码和数据,现在要以全新的视角看待进程:内核视角
以内核的视角看待进程:进程是承担分配系统资源的基本实体
Linux进程 = 大量的task_struct + 一个虚拟地址空间 + 页表 + 一部分的物理内存;
我们之前篇章所谈的进程 = 一个task_struct + 一个虚拟地址空间 + 页表 + 一部分的物理内存
一个进程的创建:必定要花费相应的资源;
之前所谈的进程也是承担分配系统资源的基本实体,只不过,该进程的内部只有一个执行流(一个 task_struct),而现在所谈的进程也是承担分配系统资源的基本实体,只不过,该进程内部有多个执行流(多个 task_struct),进程的内部允许只有自己一个执行流,也可以允许有多个执行流,我们之前介绍的进程,内部只有一个执行流,以前所讲的 “进程” 只是一个子集,今天所讲的进程才是全貌;
线程的优点
- 共享资源:同一进程内的线程共享地址空间,能访问相同的全局变量,堆和文件描述符等。这种共享使得线程间通信变得更加高效。
- 独立的执行流:每个线程都有自己的程序计数器、寄存器和栈,这使得线程可以独立执行。线程的独立性使得多个线程并行执行多个任务,提高了此程序的响应性和吞吐量。
- 轻量级:相比于进程,线程的创建开销会小很多,且不需要分配独立的地址空间。上下文切换也比进程快,因为不涉及地址空间的切换。
- 并发执行:在多核处理器上,不同的线程可以做到真正的并行执行
线程的缺点
- 同步复杂性:由于共享进程空间,多个线程同时访问和修改共享数据时可能会导致数据不一致等问题
- 性能损失:使用锁和其它同步机制会导致性能下降
- 调试难度提高:编写和调试一个多线程程序要比单线程程序困难得多
线程异常
- 单个线程如果出现除0或者访问野指针等问题导致线程崩溃,进程也会随着崩溃
- 进程终止,该进程的所有线程都会终止。这也就意味着,如果某一个线程出了异常进而终止进程的话,其它的线程也都会被终止。这也是线程不安全的原因之一。
线程和进程
进程是资源分配的基本单位,而线程是调度的基本单位。
线程共享以下进程资源:
- 代码段和数据段
- 文件描述符表
- 每种信号的处理方式即handler表
- 环境变量包括当前工作目录
- 用户id和组id
虽然线程共享进程的数据,但有属于自己的一些数据:
- 线程ID
- 一组寄存器:(硬件上下文的数据,线程切换时 寄存器会保存上下文数据-动态运行)
- 栈:线程运行时,会形成各种临时变量,临时变量会被保存在各自线程的栈区
- errno错误流
- 信号屏蔽字
- 调度优先级
三,OS和用户对于"线程"的认识区别
OS 只认线程,用户也只认线程,没有轻量级进程的概念,所以Linux无法直接提供线程操作的系统调用接口,而只能给我们提供轻量级进程的接口。
但是用户只认线程,所以Linux给我们提供了一个线程库,这个库是用户级线程库,它底层是调用轻量级进程的接口的,这个线程库对这些接口进行封装,上层用户使用这个库看起来像是Linux拥有线程一样
这个线程库的名字叫 pthread,是用户级线程库
任何的Linux系统,必须要提供这个库,这个线程库是默认携带的;这个线程库也称为原生线程库
所以用户需要关心这个线程库所提供的接口,不用关心底层的接口
三,创建线程
pthread_create - create a new thread(创建一个新线程)
头文件:#include <pthread.h>
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数
第一个参数thread,代表线程ID,是一个输出型参数,pthread_t是一个无符号整数
第二次参数attr,用于设置创建线程的属性,传入空表示使用默认属性
第三个参数start_routine,是一个函数的地址,该参数表示新线程启动后要跳转执行的代码
第四个参数arg,是start_routine函数的参数,用于传入
返回值
成功返回0,失败返回错误码
第三个参数说明:void *(*start_routine) (void *)
- 该参数是一个函数指针,用于设置一个回调函数start_routine
- 该函数的返回值是 void*,
- 函数参数是 void*,该参数由第四个参数 arg 传入
记得添加库
test:test.cpp
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf test
void* handler(void* arg)
{
while(true)
{
cout<<"new thread..."<<endl;
sleep(1);
}
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
pthread_t pid;
int n = pthread_create(&pid,nullptr,handler,(void*)"thread one");
if(n!=0) //创建失败
{
perror("pthread_create!\n");
return 1;
}
while(true)
{
cout<<"main thread ..."<<endl;
sleep(1);
}
return 0;
}
进行运行,发现主线程和新线程在同时运行
ps axj | head -1 && ps axj | grep test | grep -v grep
查看轻量级进程
查看轻量级线程相关信息的命令:ps -aL
LWP(Light Weight Process)就是所谓的轻量级进程
- 每个轻量级进程的PID都是一样的
- 每个轻量级进程LWP的ID都是不一样的
所以,CPU在调度的时候,是以 LWP 的ID作为唯一的标识符用来标识一个执行流的,并不是使用PID
这就是以前写的单执行流的代码,只有一个执行流的时候,PID和LWP是等价的;
注意:信号是整体给进程发送的,不能单独发给一个 LWP
证明第四个参数是传入给第三个参数
pthread_create 函数的第一个参数是一个输出型参数,返回的是线程ID,pthread_t 是一个无符号整数。
下面进行验证,该参数输出的是
typedef unsigned long int pthread_t;
这个id十六进制地址和LWP的id是什么关系,等线程控制详细介绍
四,进程VS线程
进程和线程切换时,消耗是不一样的;
- 进程切换:需要切换页表 && 切换虚拟地址空间 && 切换PCB && 切换上下文数据
- 线程切换:需要切换PCB && 切换上下文数据
- 线程切换cache 不用更新太多,但是进程切换需要全部更新cache(主要体现在这点)
cache
cache是集成在CPU里面的,是一个硬件,是CPU很重要的组成部分,它具有数据保存的功能,它的缓存速度比寄存器慢,比内存快
寄存器读取数据是直接在 cache 里面读取的,不是直接从内存读取,一个进程只有运行一段时间后,cache 里面才会缓存大量的热点数据;
热点数据就是:进程经常使用、经常访问、经常命中的数据(需要进程跑一段时间才会存在大量的热点数据),热点数据是被整个进程共享的;;
- 线程切换的时候,cache内缓存的数据不用切换(线程的数据共享相同的缓存),线程需要用到新的数据直接缓存进cache即可
- 而进程切换,cache内缓存的数据需要全部切换,新切换的进程需要重新缓存数据,这样效率就比线程慢得多了
- 进程切换可能导致缓存中的数据失效,因为新进程可能不会使用旧进程缓存的数据。
五,验证线程的缺点
最主要的一个缺点,也是它的优点导致,因为多个线程共享内存,这也导致了如果一个线程崩溃了,该进程内的所有线程都会崩溃,新线程也会影响主线程。因为这些线程都在一个进程当中,当进程出问题时,操作系统会杀掉当前进程,释放内存;
缺乏健壮性;
注意:信号是叫做进程信号,是整体发给进程的
理解LWP与TID
LWP本质是一个描述线程的一个概念,因为linux中没有单独为线程设计一套管理方案,又为了与进程PCB区分开(进程和线程实际上都被看作是task_struct的实例),就用LWP来表示线程。LWP和普通进程一样,由内核调度和管理。而TID是内核用来标识LWP的唯一标识符。
所有的 LWP 共享同一个进程的地址空间、打开的文件描述符、环境变量等。