并发实现机制-2-互斥实现
并发核心问题是对资源的互斥需求,互斥的实现有多种方法(硬件方法和软件方法)
互斥的方法
- 专用机器指令,较少开销但不通用。(硬件支持)
- 操作系统或程序设计语言提供某种级别的支持。(软件方法:操作系统和程序设计语言的支持)
- 进程承当实现互斥的责任,这是软件方法,增加了开销并存在缺陷。(软件方法:进程承担责任)
其中方法1的几种实现有:中断禁用、专用机器指令;2的几种方法有:信号量、管程、消息传递等;方法3的几种实现有Dekker算法、Peterson算法、竞争条件和信号量等。这几种方法的详细说明将在本篇文章总结。
硬件支持
硬件实现的几种方法有:中断禁用,专用机器指令
中断禁用:只需要保证一个进程不被中断即可,通过内核提供启用中断和禁用中断的原语来支持,但是代价很高,执行的效率会明显降低,另外不适用于多处理器
专用机器指令:compare and swap指令,用一个测试值(testval)检查一个内存单元(*word)。如果这个内存单元的当前值是测试值(testval),就用新的值(newval)替换该值,否则保持不变。(当word为0可以进入临界区,出临界区把word重置为0,如果当前有一个进程在访问临界区,其他进程的testval是1,compare and swap返回的是1,如果没有任何进程在临界区,testval为0,compare and swap返回的是0,如果检查到返回的值和传入的testval不相等,说明word的值在上一次循环被更改),当有进程处于临界区时,其他进程处于忙等待(自旋等待),
机器指令只解决了互斥,可能饥饿(选择进入临界区的进程是任意的)和死锁(如果P1在临界区被中断、并且P1在临界区正在持有一个资源R的访问,那么如果P2此时被调度执行并且试图访问R时就会失败,如果P2比P1的优先级高,P1永远不会被执行,P2永远处于忙等待,陷入死锁)
软件方法:操作系统/程序语言支持
todo: 表5.3 一般常用的并发机制截图
信号量 semaphore
二元信号量 binary semaphore,值只能是0或者1
struct binary_semaphore{
enum {zero,one} value;
queueType queue;
}
void semWaitB(binary_semaphore s){
if(s.value==one){
s.value = zero;
}else{
/*把当前进程插入队列*/
/*阻塞当前进程*/
}
}
void semSignalB(binary_semaphore s){
if(s.queue is empty()){
s.value = one;
}else{
/*把进程P从队列移除*/
/*把进程P插入就绪队列*/
}
}
计数信号量counting semaphore(一般信号量 general semaphore)
struct semaphore{
int count;
queueType queue;
}
void semWait(semaphore s){
s.count--;
if(s.count<0){
/*把当前进程插入队列*/
/*阻塞当前进程*/
}
}
void semSignal(semaphore s){
s.count++;
if(s.count<=0){
/*把进程P从队列移除*/
/*把进程P插入就绪队列*/
}
}
信号量解决互斥问题的写法如下
const int n= /*进程数*/;
semaphore s = 1;
void p(int i){
while(true){
semWait(s);
/*临界区*/
semSiganl(s);
/*其他部分*/
}
}
void main(){
parbegin(P(1),P(1),...,P(n));
}
信号量的实现
semWait和semSignal的实现必须是原子原语,任何时候只有一个进程可用semWait或者semSignal操作控制一个信号量。
可以使用软件方案:dekker算法或者peterson算法,但这有处理开销
可以使用硬件方案:comapare&swap和中断禁用(单处理器系统),因为semWait和semSignal的操作时间很短,因此忙等待或者中断禁用的时间都分长短,是比较合理的。
实现方法如下(注意important语句,并注意堵塞进程时的处理,因为堵塞进程时进程无法到达最后一句,所以堵塞时就应该执行flag置为0或者允许中断!!!)
void semWait(semaphore s){
while(compare and swap(s.flag,0,1)==1); // important ! 或: 中断禁用
s.count--;
if(s.count<0){
/*把当前进程插入队列*/
/*阻塞当前进程,并设置flag为0或允许中断*/ // important !
}
s.flag=0; // important ! 或: 中断允许
}
void semSignal(semaphore s){
while(compare and swap(s.flag,0,1)==1); // important !
s.count++;
if(s.count<=0){
/*把进程P从队列移除*/
/*把进程P插入就绪队列*/
}
s.flag=0; // important !
}
-
PV操作: P操作用于semWait V操作用于semSignal
-
进程按照什么顺序从队列中移除?FIFO最公平,被堵塞最久的进程最先从队列释放,采用这一策略的信号量是强信号量,没有规定队列移出顺序的信号量为弱信号量。强信号量确保了不会饥饿。
-
二元信号量与互斥锁(mutex)的区别:为互斥量加锁的进程和解锁的进程必须为同一进程,而二元信号量可以由一个进程加锁,而另一个进程解锁
-
信号量本质上处理的是进程进入和移出 阻塞和就绪队列
-
信号量一般初始化为1,这样第一个执行semWait(s)的进程可以立即进入临界区,并把s的值置为0,接下来任何试图进入临界区的其他进程都会被堵塞。
程序也可以公平的处理一次允许多个进程进入临界区的需求,这个需求可以把信号量初始化为某个特定的值来实现。无论何时s.count的值可以解释如下
(count大于1的值的情形例子如哲学家就餐问题,虽然count大于1不能保证临界区互斥,但是没有告诉我们不能再临界区中再加一个信号量来保证互斥,外层count大于1的信号量用于限制临界区进程数量,第二个count等于1的内层信号量用于保证互斥)
- s.count>=0:s.count是可以执行semWait(s)而不被堵塞的进程数。这种情形允许信号量支持同步和互斥。
- s.count<0: s.count的大小是堵塞在s.queue队列中的进程数
管程
使用信号量设计程序是困难的,难点在于semWait和semSignal操作可能分布在整个程序中,而且很难看出信号量在这些操作上产生的整体效果。
管程是一种结构,可以用管程来锁定对象。
管程是由一个或多个过程(函数、方法)、一个初始化序列和局部数据组成的软件模块。(更加面向对象),管程特点如下
- 局部数据变量只能被管程的过程访问
- 只能调用管程的一个过程来进入管程
- 在任何时间,只能有一个进程在管程中执行,调用管程的任何其他进程都被堵塞,
进程不仅能够在管程中被堵塞,也能够释放这个管程,当条件满足且管程再次可用时,需要恢复该进程并允许它在堵塞点重新进入管程。
管程使用条件变量来支持同步,cwait(c),csignal(c),如果管程中的一个进程发信号,但是没有在这个条件变量c上等待的任务,那么丢弃这个信号
管程优于信号量的地方在于,所有的同步机制都被限制在管程内部,因此不但易于验证同步的正确性,而且易于检测出错误????
-
使用信号的管程 ??? 未
-
使用通知广播的管程 ??? 未
消息传递
xx 未
和socket的关系???