线程概念
什么是线程?
CPU视角:
线程是CPU调度的基本单位
与进程的关系:
线程是进程内部的执行流,线程比进程粒度更细,调度成本更低,切换成本更低
线程在进程内部运行,本质是在进程地址空间内运行,一个进程至少有一个执行线程
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
Linux下线程概念
在Linux系统的CPU眼中,看到的PCB其实就是轻量化进程,看图理解:
将上图的所有的task_struct,进程地址空间,页表这三者之和称为进程,而对于每个task_struct就是线程。所以创建线程时只创建PCB,线程只占用进程地址空间中的一部分代码和一部分数据。
站在OS的角度重新理解进程:进程是承担分配系统资源的实体 (向系统申请资源的基本单位) 。
对于只有一个task_struct的进程就是单执行流进程,不止一个task_struct的进程就是多执行流进程。
那么CPU能分辨task_struct是进程和线程吗?
答案:不能也不需要,在Linux中进程和线程没有概念上的区分,都叫执行流,CPU只需要关心一个个独立执行流。无论进程内部只有一个执行流还是有多个执行流,CPU都是以task_struct为单位进行调度的。
类比理解:进程和线程关系就好像家庭和家庭成员,家庭是分配社会资源的基本载体,家庭成员占一部分资源。
站在CPU的视角:task_struct <= 传统的进程(当且仅当进程是单执行流,等号成立),也就是所CPU看到的PCB比传统的进程更轻量化了 。
理解页表
为何以多级页表实现?
以32位平台为例,会产生232个地址,倘若是一张页表建立虚拟地址和物理内存的映射,则需要232个页表项,页表项中还包含权限相关的信息,比如字符串常量不能修改,因为页表中有读写权限标识了变量的读写权限,这样一张页表的占据空间是巨大的,所以Linux下的页表是以多级页表实现的。
多级页表是如何实现的?
以32位平台为例,多级页表的组成:页目录 + 页表,将32位地址分成3部分=10+10+12
页目录:虚拟地址前10位 映射到-> 页表
页表:虚拟地址的中间10位 映射到-> page的起始地址(页框4KB)
画图理解:
物理内存被分成了一个个4KB大小的页框page,可见对内存的管理就是对数组struct page mem[1024*1024]的管理。
多级页表的优点
1.将进程虚拟地址管理和内存管理,通过页表+页框page解耦
2.使用分页机制和按需创建页表,节省了内存空间
多线程的特点
优点
线程创建:创建线程的代价比创建进程的代价小很多
线程切换:与进程切换相比,线程需要OS做的工作少很多
线程占有资源:线程占用的资源比进程少很多
利用多处理器:能充分利用多处理器的可并行数量
并行处理任务:在等待慢速IO的时候,程序可以执行其他计算任务
计算密集型应用:在多处理器系统上运行,不同CPU执行不同的计算线程
I/O密集型应用:将IO操作重叠,不同线程等待不同的IO操作,提高性能
缺点
性能损失:计算密集型应用的计算线程数量大于处理器数量,较大的性能消耗,增加额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程程序需要更全面深入的考虑,因为时间的影响导致不该共享的变量共享了会造成影响,因为多个线程使用同一个地址空间,会相互影响,又称线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,线程的IO函数会影响整个进程,比如不同同时往显示器打印
编程难度提高:排查问题难度更大,编写多线程程序比单线程程序更难
线程异常
当一个线程异常的时候,会导致整个进程退出,因为线程异常的时候,就是进程异常了,进程就会收到OS发来的信号,进而进程退出。当创建新的线程异常的时候,新线程异常退出的信号是整个进程的,主线程不用主动获取。线程异常会使异常的线程影响其他的线程,所以线程的健壮性较低。
线程用途
1.提高CPU密集型程序的执行效率
2.提高IO密集型程序的用户体验
进程和线程的关系
进程是OS分配资源的基本单位,线程是CPU调度的基本单位,因为线程共用一份地址空间,因此代码段(Text Segment、Data Segment),数据段都是共享的,定义函数、全局变量,在各线程都可以看到,除此之外,线程还共享:
文件描述符表
每种信号处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
当前工作目录
用户 id 和 组 id
线程共享进程数据,也拥有自己的一部分数据:
线程 ID
一组寄存器
线程栈
errno(错误码)
信号屏蔽字
调度优先级
线程控制
原生线程库:Linux下有一套遵从POSIX标准的线程库,与线程有关的函数构成了一个完整的序列,绝大多数函数名以pthread_开头,使用函数时需要引入头文件<pthread.h>,编译的时候需要使用-lpthread选项表示要链接线程库pthread。在C++11中也更新了线程库,C++的线程库也是封装了这套原生线程库的。
创建线程
使用函数pthread_create创建线程,函数原型:
参数说明:
thread:输出型参数,表示线程id
attr:设置创建线程的属性,传入NULL表示使用默认属性
start_routine:线程执行的函数,该函数的返回值类型是void*,作为线程等待的输出型参数
arg:线程执行的函数传入的参数
函数返回值:创建成功返回0,创建失败返回错误码
等待线程
使用函数pthread_join等待退出的线程,倘若不做此工作也没有分离线程,就会造成类似僵尸进程般的内存泄露。
函数原型:
参数说明:
thread:等待的线程id
retval:输出型参数,输出进程的退出码
函数返回值:等待成功返回0,错误时返回错误码
线程退出
退出方法:
1.在线程运行函数中return
void*threadRoutine(void*args)
{
...
return nullptr;
}
2.线程调用函数pthread_exit主动退出
3.线程调用pthread_cancle取消其他线程(通常是主线程调用)
注意:线程调用exit函数会退出进程,任何一个线程调用都代表整个进程退出。
线程分离
使用pthread_detach函数分离指定线程,相当于告诉OS线程退出时自动释放线程资源
可以让其他线程分离,或者自己主动分离。让主线程分离时,主线程退出相当于进程退出 ,分离主线程后,主线程一般不退出(常驻内存)。
线程分离相当于线程退出的第四种退出方式, 延后退出。
线程id
线程调用函数pthread_self可以获取线程的线程id
代码实例:
#include <pthread.h>
#include <iostream>
#include <unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
for (int i = 0; i < 20; i++)
{
cout << "[" << pthread_self() << "] pthread " << (int)i << "is running..." << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tp[3];
for (int i = 0; i < 3; i++)
{
pthread_create(&tp[i], nullptr, threadRoutine, (void *)i);
}
while (1)
{
cout << "main thread is running!" << endl;
sleep(1);
}
return 0;
}
编译运行程序,使用ps -aL查看线程,L表示查看轻量级进程(LWP)。
可以看到多个线程往显示器打印出现了混乱。上图的LWP,是OS标识轻量级进程的编号,即线程:LWP=1:1。
线程是独立执行流,在运行过程中会产生临时数据,需要有自己独立的栈结构,所有的代码执行都在进程的地址空间中,线程的全部实现,并没有体现在OS内,而是OS提供执行流,具体的线程结构由库来管理,在进程地址空间加载的共享区中的线程库维护着线程结构,每个用户级线程的控制结构体的起始地址就是线程id,如图:
线程的局部存储指的是线程私有的全局变量,指定线程的代码中在全局变量前加上 __thread可以使变量变成私有的全局变量。
链接的线程库:
线程控制块中有私有栈,底层是通过调用clone函数实现的:
这个函数可以创建共享内存空间的进程,其实是Linux下的线程的原型。
另外使用syscall(SYS_gettid)间接调用gettid 可以获得LWP,如下:
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t tid;
tid = syscall(SYS_gettid);
tid = syscall(SYS_tgkill, getpid(), tid);
}
另外使用syscall(SYS_gettid)间接调用gettid 可以获得LWP,如下:
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t tid;
tid = syscall(SYS_gettid);
tid = syscall(SYS_tgkill, getpid(), tid);
}