线程的管理
为什么需要线程管理
线程管理是对某个程序中全部线程的管理工作,当不同进程对同一资源进行操作时,可能会出现以下问题:
- 安全性问题
- 活跃性问题
- 性能问题
为了使程序可以健壮,稳定,高效的运行,我们需要对线程进行管理。
安全性问题
那么线程的安全性问题是什么?对于程序来讲,在运行时发生错误就是不安全的。所以,对于线程来讲,安全性问题即运行结果的“正确性问题”。
最常见的例子就是:
1 | count++; |
这个操作其实分为三步:
- 首先获取count的值;
- 将count的值+1;
- 将+1后的结果赋值给count;
当两个线程对同一count变量执行该操作时,由于执行顺序有很多种,所以会产生不同的结果,也就可能会产生错误的结果,这就是线程的安全性问题。
活跃性问题
什么是活跃性问题?当一个线程由于某些原因一直没有被执行,或者所分到的时间片非常少,那么这个线程就存在着活跃性问题。
常见的活跃性问题包括:死锁,饥饿,活锁。
死锁
线程A在等待B持有的某种资源(这里是锁),而线程B在等待A持有的某种资源,在没有获得自己想要的资源之前,线程A和线程B都不会主动的释放自己所持有的资源,于是就陷入了“死锁”;
形象一点来讲,就像是幼儿园的两个小朋友吵架了,两个小朋友心理都在想“哼!╭(╯^╰)╮如果你不道歉,我就不道歉”,于是局面就一直僵持着。
还有一种更简单的情况,也是死锁
有A和B两个线程,A和B运行都需要4份资源,总共有6份,此时A和B都获取了3份,此时两者都没有满足运行条件,则产生死锁
形成死锁的条件
- 互斥条件:一个资源只能由一个进程使用
- 部分分配条件:一个进程占有一定资源后,期间又在申请其他资源
- 不可抢占条件:一个资源只能被释放,不能被抢占
- 循环等待条件:一个进程占有若干资源,同时等待其他资源,多个进程形成循环等待链
利用银行家算法避免死锁
在银行家算法中,资源的分配是批量的,仅仅在申请者获得资源后可以运行,系统才会为其提供资源。
例如上述的例子,A和B两个线程,系统会一次性分配4份资源给A或B,让其执行。
等待图法检测死锁
如上图所示,P1等在P2占有的资源,P2等待P3……,当等待图中形成回路时,代表产生了死锁。
活锁
活锁概念:独木桥两个人互相让路。
在计算机中活锁的情况不太常见。这里就不展开讨论。
饥饿
人的饥饿是因为没有食物吃,而线程的饥饿时因为没有CPU时间片使用,产生这种情况的原因可能是:
- 线程的优先级太低
- CPU的调度算法存在问题,或者不适合该程序
性能问题
正如字面意义,如果存在性能问题,说明服务时间过长,相应不灵敏,吞吐量低等。
在线程方面出现这类问题的原因通常是“滥用锁”;
引入多线程的目的就是因为程序中的多种任务可以并行执行(参考单核多线程的CPU),但由于频繁加锁,或者锁的细粒度很大,会导致程序很长一部分时间都处于单线程状态,自然会产生性能问题。
如何解决上述问题
有两种经典有效的办法解决上述问题,“正确的”使用PV操作或管程。
- 对于安全性问题,产生的原因是不同线程对共享变量的操作时机不同,所以我们只需要保证某一时刻只有一个线程操作共享变量即可,即线程互斥;
- 对于活跃性问题,往往是由于某个线程不能得到他需要的某个资源,所以我们要做好“线程间的同步”工作,即线程同步;
- 对于性能问题,往往是由于“锁的滥用”,所以我们要尽量使用小粒度的锁。
PV操作和管程
PV操作
原语:所谓原语,即不可中断的执行过程
一,信号量
信号量:表示一种资源能否被使用。其数据结构表示如下
1 | semaphore{ |
注意,信号量只能只能三个操作
- 初始化
- 被P操作
- 被V操作
信号的设置,往往是针对操作的
二,P原语
P代表进程申请使用资源,申请后,资源的信号量value值-1
- 如果此时value<0,则将此进程插入到阻塞队列中
- 如果value在-1后≥0,则执行该进程
三,V原语
V代表进程归还(释放)资源,释放后,value值+1
- 如果+1后,value≤0,则说明原来是小于0的,说明阻塞队列中有等待资源的进程,+1代表释放一个资源,所以要执行一个阻塞队列中的进程
- 如果+1后,value>0,则说明没有被阻塞的进程,只是归还一个待使用的资源而已
管程
管程可以看做是一个盒子,他把共享变量和对共享变量的操作封装到一起,并且盒子(管程)中只有一个位置,即同一时刻只允许一个进程进入管程。
这里可能看起来很抽象,具体的使用方法请参考下文。
进程互斥
什么是进程互斥?进程互斥就是不允许两个进程同时进入临界区(共享内存),去操作临界资源(共享资源)。
使用PV操作解决互斥问题
- 定义信号量mutex,由于是实现互斥,所以初始值为1(表示只有一个资源可以被使用)
- 使用P操作检测mutex
- 使用V操作检测mutex
程序的实现大致如下:
1 | semaphore mutex; |
使用管程解决互斥问题
如上图所示,由于管程中的预留位置为1,所以同一时刻只能有一个线程进入管程。那么管程是怎么保证只有一个的呢? 因为管程使用了 入口等待队列 。要想进入管程就要先进入队列排队。
这样就保证了同一时刻只有一个线程操作共享变量,解决了线程互斥的问题。
进程同步
什么是进程的同步?同步表示不同进程种相互制约的执行关系。即要想执行某项任务,必须满足某个前提条件。
使用PV操作解决同步问题
同步的信号量往往是针对操作本身的(也就是说该操作是否可以被执行)
而互斥的信号量往往代表一种资源是否可以被使用
最常见的同步例子:生产者-消费者,缓冲区大小为n,消费者如果想要进行消费,那么商品队列中一定要有商品,具体实现如下:
使用管程解决同步问题
- 首先假设消费者线程先进入管程中,要进行消费执行decrease操作的条件变量是full=true,但是当前条件不满足,于是此线程被加入到对应条件变量的等待队列中
- 此时生产者线程进入,发现empty=true,于是执行increase操作
- 在执行increase后,条件变量full=true,于是释放相应队列中的线程
同步和互斥结合
生产者-消费者(这里的缓冲区大小要求为K[k大于1])
- 这里的同步指的是:
- 消费者在消费前,队列中要有产品;
- 生产者在生产前,产品队列不满;
- 互斥指的是对临界资源的互斥操作,既同一时刻只能有一个线程对产品队列进行操作