最后他的身躯一点点消散,留下了苍老的浮云和涛声,而他的血管,业已成为那条河的支流。
基本介绍
定义
线程是CPU进行调度和分配的基本方式,一个CPU在一个时间只能运行一个线程。我们要注意程序为何要设计成多线程的,在一开始CPU都是单核的,其实这种情况下程序执行的最快的方式就是单核单线程,但是这样会带来并发问题,例如当处理IO时,如果只是单线程的,就会导致阻塞,此时CPU无事可做,所以我们开始将程序设计为多线程,从而实现并发。给我们一种错觉,就是同一时间有多个程序在执行。
线程的特点及优点
特点
线程是轻量化的进程,其特点为:
- 共享代码、数据、文件和内存地址
- 独有寄存器(程序计数器)和栈空间,因为每个线程执行速度是不一样的
优点
单CPU
充分利用CPU,令$t_i$为线程1的空闲时间,$t_s$为上下文切换时间,那么当满足下面公式时,进行多线程可以提高CPU利用率。一般来说,线程切换比进程切换开销小很多。
多CPU
- 加速
- 专业化,每个线程做特定的工作,也可以保证hot cache
支持线程机制的要素
- thread data struct:和PCB一样,用于对线程进行描述。
- 创建和管理线程的机制
- 多线程合作的机制:由于多线程共享内存地址,所以会导致同步问题
Thread data structure
一个完善的线程数据结构应包括:线程ID、程序计数器、栈顶指针(SP)、栈段以及其他的一些关键属性。
线程创建和管理
fork
fork,创建一个线程而非进程,线程创建的同时,还会初始化线程数据结构。
join
当父线程t0对子线程t1调用join时,join(t1)
,那么父线程会被阻塞,直到子线程返回。同时,join将会返回子线程的计算结果。
detach
一个detach的线程被创建后,父子线程是相互独立的。
线程创建的例子(伪代码)
1 | Thread thread1; |
这个代码没有考虑线程调用顺序,因此会导致结果的不确定性。
内核态和用户态线程
内核态和用户态中都可以有线程。内核态的线程可以由内核访问和管理。调度器会决定哪个线程运行在哪个CPU上。一些内核线程可能在支持用户态线程的工作,另一些可能在执行一些内核的工作。一个用户态线程必须和一个内核线程关联后,才能被正确调度和运行。本节将针对内核态和用户态线程的调度模型进行总结。
内核线程与用户线程的区别
- 内核线程只运行在内核态,而用户线程可以运行在用户态,也可以运行在内核态(系统调用时)
- 内核线程一般作为守护线程,执行一些诸如页表换入换出、脏页回写、软中断处理等操作
- 内核线程也会参与调度,每个CPU上有一个IDLE线程,实际是一个优先级最低的内核线程,实在没有任务了就执行这个
- init是所有用户进程的祖先,而kthreadd是所有内核线程的祖先
用户态多线程模型
一一对应模型
在该模型中,一个用户态线程和一个内核态线程是一一对应的关系。当一个用户态线程被创建后,就会有一个新创建或已经存在的内核线程和其一一对应。
优点
并发性好,内核可以知道用户到底有多少个线程,从而可以动态调整内核线程数目和用户态进程中的多线程相匹配
缺点
- 必须在操作系统中完成所有操作,而且要频繁在用户态和内核态切换,代价大。
- 只能依赖内核态的线程调度策略,这也限制了可移植性
多对一模型
在该模型中,多个用户态线程和一个内核态线程对应,由用户指定调度策略,指定规定时间哪个线程和内核态线程对应
优点
可移植性强,不依赖内核态线程调度策略
缺点
- 由于没有用到内核线程的特性,所以操作系统对于用户的多线程程序基本上没太多帮助,操作系统也不知道用户到底有几个线程
- 一个用户线程的阻塞将会导致整个进程的阻塞
多对多模型
该模型是上面两种模型的综合,一些线程一一对应,另一些和内核多对一对应
优点
综合了上面两种的优点
缺点
对于内核和线程的管理更加复杂,需要一个专门的用户-内核态线程管理器
用户态和内核态线程调度
在用户态中,由一个用户层面的库对于一个进程中的线程进行管理,而在内核态中,由CPU调度器对内核态线程的调度和资源分配进行管理
Pthreads(Posix thread)
pthreads(Portable operating system interface)是Unix系统下常用的线程API,本节将针对pthreads的使用进行介绍。
创建
常用数据结构及API
创建thread对象
1 | //-----------------------thread对象 |
创建线程
1 | int pthread_create(pthread_t *thread, |
返回值表示是否创建成功,可以根据attr设置创建线程的属性。
join
1 | int pthread_join(pthread_t thread, |
返回值表示join是否成功
detach
1 | // 方式1 |
exit
1 | int pthread_exit(); |
设置属性
1 | //----------------------- attr 处理 |
例子:创建多线程程序
无参数线程函数
1 |
|
编译过程为:gcc -o main main.c -lpthread
有参数线程函数
1 |
|
Mutex
pthreads 对于锁的使用如下所示:
1 | pthread_mutex_t aMutex; //创建锁 |
注意:Mutex不能用在中断场景下,因为会引起睡眠
条件变量
1 | pthread_cond_t aCond; //创建 |
Linux下线程的实现
共享与独占资源
在linux下的线程中,会有自身独有和共享的资源,这里对这些资源进行一个总结,其中,共享的资源包括:
- 虚拟内存映射
- 进程基础信息
- 一些数据
- 打开的文件
- 信号处理
- 当前工作目录
- 用户和用户组属性
- ……
独占的资源包括:
- 线程ID
- 一系列寄存器
- 栈
- 错误码
- 信号掩码
- 线程优先级
- ……
进程与线程的联系与区别
联系
线程实际上是将进程的指令执行流部分进行了剥离,进程负责资源调度,而具体指令执行交给了线程,两者的关系可以用下面的图进行概括:
可以看的线程是有自己独立的堆栈和SP指针,以及相应的寄存器等。所以线程实际就是进程减去资源共享
区别
- 进程是操作系统进行资源分配的最小单位,而线程是CPU进行任务调度的最小单位。一个进程至少包含一个线程
- 进程有独立的地址空间,每次启动一个进程,系统会分配地址空间,同时建立数据表维护代码、数据、堆栈段,开销很大。而线程共享代码段(代码与常量)、数据段(全局变量与静态变量)和堆段,开销小很多,但是每个线程有自己的栈段(局部变量和临时变量)。创建与切换线程的开销比创建切换线程大很多。
- 线程间通信更方便,同一进程下的线程共享全局变量、静态变量等数据,进程间通信需要以通信方式进行,但是多线程需要处理好同步与互斥。
- 多进程程序更健壮,一个进程的消亡不会影响另外的进程;而多线程的程序有一个线程死掉,整个进程就会死掉。