操作系统中的线程、锁机制和PV操作
线程(Thread)
线程是可由CPU直接运行的实体,是程序中的执行路径,一个进程可以创建多个线程,多个线程共享CPU可以实现并发运行。同时线程的并发粒度比进程的并发粒度更细。
单线程程序
整个进程中只有一个线程。Window程序缺省只有一个线程(主线程,main线程)
多线程程序
整个进程中至少有两个线程。主线程和至少一个用户线程。
线程技术的典型应用场景
程序的多个功能需要并发运行
在线视频:
并发的功能:视频的解码、音频的解码,网络接受,显示和播放音视频等等,这些都需要并发运行,才能给我们良好的体验感和沉浸感。
Microsoft Windows的文件拷贝程序是个多线程程序
多核心CPU上的应用,利用多线程可以更好的发挥多核性能
使用多线程的麻烦事儿
虽然使用线程可以帮我们解决很多问题,线程也存在着一些问题,需要克服,第1个就是一个程序来说,如果单线程程序来说程序是依次往下运行的,逐行逐行的,往下运行的,如果是多线程程序,那么程序的运行是并发的,所以这个程序在调试的时候,它就不一定是按照这个我们预想的一行一行的运行,过程难以控制,同时存在那么多个并发运行的线程,多线程可能会同时对这个共享变量进行一个程序操作,会不会多个线程的同时来访问?这就回带来很多的麻烦事儿;另外就是线程的安全问题。
临界区和锁
临界区和临界资源
并发错误:对于一个内存全局变量k,有程序a和程序b如下:
程序A:...
k = 100;
...
printf("A : k = %d",k);
...
...
程序B:...
k = 200;
...
printf("B : k = %d",k);
...
...
如果两个程序并发运行,两个程序的打印结果是不确定的,既可以是100也可以是200。
假设程序A在执行 printf()
语句之前由于需要其他操作,暂时停止运行,而程序B无阻碍的运行,就会在程序A打印变量 k
之前将其值修改为200,进而使得程序A的打印结果不再是理论上的100.这就会出现程序的并发错误。
那有什么好的解决方案吗?
答案:程序设定一个特定的区域不让两个程序同时进入运行,只能先后进入。
临界资源
一次只允许一个进程独占访问和使用的资源(如内存中的共享变量)
临界区
进程中访问临界资源的程序段。
临界区和临界资源的访问特点
- 具有排他性
- 并发进程不能进入到临界区
设计临界区访问机制的原则
既然临界区对程序能够正确地并发运行很重要,那么在操作系统中,就需要设计出合理而高效的临界区访问机制,来达到这一目的,而在设计访问机制时需要有一定原则。
忙则等待
当临界区忙时,即是已经有进程进入到了临界区,其他进程必须在临界区外等待当前进程在临界区中完成运行。
空闲让进
当无进程处于临界区时,任何有权进程都可以进入临界区。
有限等待
进程进入临界区的请求应在有限时间内得到满足。简单来说就是假设一个进程A进入了临界区执行,此时进程B在临界区外等待,但是操作系统不能让进程B 长时间的等待,使其处于资源缺乏的饥饿状态。
思考: 临界区的设置大些还小些好?
答案: 临界区的设置不能无根据的扩大,这样会使得其他需要进入该临界区的进程的等待时间变长,临界区的大小设置是根据不同进程的实际情况而定,总之在不影响进程执行得到正确结果的情况下,越小越好。
让权等待
等待进程放弃CPU(让其他的进程有机会得到CPU)
当一个进程到达临界区时,CPU会询问其是否可以进入临界区,当不能进入时,此时该进程进入等待状态,此时它应该放弃CPU,将控制权交还给操作系统,调度程序调度其他进程进行运行,而不是让CPU一直询问是否能够进入临界区。
锁机制
基本原理
- 设置一个标志,来判断当前临界区是否可以进入。
其实这可以通俗的理解为:
临界区-->卫生间、锁标志-->卫生间门(不恰当的比喻 ^ _ ^)
当一个进程需要进入某个临界区时,首先检查该临界区的标志(卫生间门是否是开着的)是否为可用,是则进入,反之等待。进程进入到临界区后,将标志更改为不可用(进入卫生间要关门),退出临界区时将标志修改为可用(出卫生间要开门)。
上锁操作
Lock(S) {//上锁原语test:if (S == 0 ) {
goto:test;//测试锁标志
} else {
S = 0;//上锁
}
}
开锁操作
Unlock(S) {//开锁原语S = 1;
}
同步和PV操作
进程的同步和互斥
互斥(一种特殊的同步关系)
多个进程共享了独占性资源,必须协调好对资源的存取顺序,确保没有两个或以上的进程同时的对资源进行存取操作。
同步
若干合作进程为了完成一个共同的任务,需要相互协调运行步伐,一个进程开始某个操作之前必须要求另外一个进程已经完成某个操作,否则前面的进程只能等待。
PV操作
信号灯机制
信号灯的数据结构
- 变量定义为一个二元矢量(S,q)
- S:整数,初值非负,又叫信号量
- q:PCB队列,初值为空集
structSEMAPHORE{int S ;//整数,初值非负
pointer_PCB q;//队列:进程PCB指针,初值为空集
}
P操作:函数或者过程,P(S,q)
- S的值减1
- 若S>=0,该进程继续执行
- 若S<0,该进程阻塞,进入阻塞队列q,交控制权给操作系统,调度函数System scheduling function
/*****************************************伪代码*********************
* S : 信号量
* q : 队列
* Caller : 调用该操作的进程
* Insert(Caller,q) : 将进程放入队列q中
* Block(Caller) : 将进程阻塞
***********************************/
P (S,q) {
S -- ;
if (S < 0) {
Insert(Caller,q);
Block(Caller);
goto:System scheduling function;
}
}
由此很容易看出:P操作可能使得调用P操作的进程进入阻塞状态;S初值的设置很重要。
V操作:函数或过程,V(S,q)
- S++
- S>0,该进程继续
- S<=0,该进程继续同时从q中唤醒一个进程
/*****************************************伪代码*********************
* S : 信号量
* q : 队列
* pid : 待唤醒的进程ID
***********************************/
V (S,q) {
S ++ ;
if (S < 0) {
Remove(q,pid);//pid:进程ID
Wakeup(pid);
}
}
一个进程调用V操作,该进程可能会唤醒一个正在阻塞的进程。
PV操作解决互斥问题
pv操作具有一定的功能和性质,我们可以利用它独有的性质去完成之前用锁机制完成的临界区互斥访问。
之前讨论过,P操作可以使一个正在运行的进程进入到阻塞状态,这就和上锁操作类似;同理,V操作可以唤醒一个正在阻塞的进程,类似于开锁操作。
举例
下图可以很好的展示一个简单的利用PV操作实现线程对临界区的互斥访问。同时信号量在访问结束后,mutex的值回到初始值。
PV操作解决进程同步问题
同步机制的实质
- 运行条件不满足时,能够让进程阻塞
- 运行条件满足时,能够让进程立即继续
P-V操作应用与进程同步的基本思路
- 暂停当前进程:在关键操作前执行P操作---->必要时可暂停
- 继续进程:在关键操作之后执行V操作------>必要时唤醒合作进程
- 定义合理并且有意义的信号量,设置合适的初值
实例:公交车司机和售票员
时间回到2010的那个年代,想必大家都做过有售票员的公交车或者客车,这里以这个场景为例,来展示如何利用P-V操作实现进程间的同步。
司机:
- 起步
- 驾驶
- 停车
售票员:
- 开门
- 售票
- 关门
同步要求:
- 只有在售票员将车门关好之后,司机才能够起步
- 只有司机停好车之后,售票员才能够打开车门
很容易就可以分析出:司机的关键操作是起步和停车;而售票员的关键操作是开门和关门。
关键操作的判断条件:
- 该操作的执行需要一定的条件(在该操作之前添加P操作)
- 该操作执行完成与否影响到合作进程的某个操作(在该操作之后添加V操作)
假设司机进程先运行,首先执行P操作,判断门是否关好,初值为0,未关好,则司机进程阻塞,售票员进程继续执行。
售票员执行关门操作:然后执行V操作:s1= 0 并且唤醒司机进程
司机和售票员并发执行驾驶和售票操作,互不影响:
假设此时即将到站,售票员试图开门,会先执行P操作:判断车是否停好,此时S2 = 0,售票员进程会被阻塞,S2 -> -1;
司机进程继续,然后执行停车操作,停好车之后,执行V操作,使得S2 = 0 ;并唤醒售票员进程:
至此,该司机和售票员的一个进程同步过程就顺利完成了。
以上是 操作系统中的线程、锁机制和PV操作 的全部内容, 来源链接: utcz.com/a/26661.html