750 likes | 1.01k Views
第 3 章 并发控制 —— 互斥与同步. 本章知识点: 3.1 并发原理 3.2 互斥 —— 软件解决方法 3.3 互斥 —— 硬件解决方法 3.4 信号量 3.5 管程 3.6 消息传递 3.7 读者 / 写者问题 3.8 系统举例(略). 3.1 并发原理.
E N D
第3章 并发控制——互斥与同步 本章知识点: • 3.1 并发原理 • 3.2 互斥——软件解决方法 • 3.3 互斥——硬件解决方法 • 3.4 信号量 • 3.5 管程 • 3.6 消息传递 • 3.7 读者/写者问题 • 3.8 系统举例(略)
3.1 并发原理 在单处理机多道程序的系统中,进程的并发执行方式是插入执行(图3.1a),表面看起来进程如同是同时执行的。在多处理机系统中并发执行方式有插入执行和重叠执行(图3.1b) 。并发的存在要求操作系统必须能跟踪大量活跃进程,必须为每一活跃进程分配资源,必须保护每一进程的数据和物理资源不被其他进程侵犯,并且进程执行的结果与其他并发进程执行时的相对速度无关(P73 例 echo)。
3.1.1 进程间的相互作用 进程之间常常相互作用,存在某种彼此依赖或相互制约(直接或间接)的关系,表现为同步和互斥关系。 根据进程意识到其他进程的存在程度不同,可将进程间的相互作用划分为:进程互不觉察、进程间接觉察、进程直接觉察(见表3.1)。
3.1.2 进程间的相互竞争 并发进程在竞争使用同一资源时将产生冲突。进程间的竞争面临3个控制问题: • 互斥—临界资源、临界区的概念 • 死锁—例如、系统中只有1个输入设备、1个输出设备,而进程P1占有输入设备、申请输出设备;进程P2占有输出设备、申请输入设备。 • 饥饿—P77例 竞争的控制不可避免地涉及到操作系统,因为是操作系统分配资源,另外,进程自身也必须能以某种方式表达互斥的要求 。
3.1.3 进程间的相互合作 1.通过共享合作 这些进程并不是通过名字察觉到对方,而是通过共享访问间接察觉。进程间通过共享方式进行合作。除互斥、死锁和饥饿外,保证数据的一致性也是一个潜在的控制问题(见P78例)。
3.1.3 进程间的相互合作 2.通过通信合作 进程通信是指进程之间可直接以较高的效率传递较多数据的信息交换方式(例如、信箱通信)。这种方式中采用的是通信机构,在进程通信时往往以消息形式传递信息。因为在消息传递中不存在共享,所以这种形式的合作不需要互斥,但是还存在死锁和饥饿问题。
3.1.4 互斥的要求 并发进程的成功完成需要有定义临界段和实现互斥的能力,这是任何并发进程方案的基础。解决互斥问题必须满足以下要求: • 互斥执行 • 执行非临界段的进程不能受到其他进程的干扰 • 有限等待 • 有空让进 • 没有进程相对速度和数目的假设 • 进程进入到临界段中的时间有限
3.2 互斥——软件解决方法 软件方法对并发进程不提供任何支持,因此,无论是系统程序或应用程序,进程都要同其他进程合作以解决互斥,它们从程序设计语言和操作系统那里得不到任何支持。软件方法易引起较高的进程负荷和较多的错误,但有利于深刻理解并发的复杂性。
3.2.1 Dekker算法 Dekker算法的优点在于它描述了并发进程发展过程中遇到的大部分共同问题。任何互斥都必须依赖于一些硬件上的基本约束,其中最基本的约束是任一时刻只能有一个进程访问内存中某一位置。
3.2.1 Dekker算法 • 1.第1种途径:两进程解法 (算法 1) • 设两个进程共享一个整型变量turn • 如果变量turn的值为 i,则进程 Pi可以在其临界区内执行 • 当 Pi退出临界区,它置turn的值为j • 确保仅有一个进程可以在它的临界区内 • 存在进程推进问题 • 如果turn的值是j,而进程j并不需要进入临界区;当进程 i 试图进入临界区时,它将被阻塞。
3.2.1 Dekker算法 var turn: 0..1; {turn为共享的全局变量} PROCESS 0 PROCESS 1 … … while turn≠0 do {nothing} while turn≠1 do {nothing};〈critical section〉; 〈critical section〉; turn: =1; turn: =0; … … 这种方法保证了互斥,但它只记住了允许哪个进程进入其临界段,未记住每个进程的状态。如果有一个进程以后不进临界区,则另一个进程也无法进入。
3.2.1 Dekker算法2.第2种途径 共享的全局变量是:var flag: array[0..1]of boolean; 它被初始化为false PROCESS 0 PROCESS 1 … … while flag[1]do {nothing}; while flag[0]do {nothing}; flag[0]: =true; flag[1]: =true; 〈critical section〉; 〈critical section〉; flag[0]: =false; flag[1]: =false; … … 有可能两个进程同时进入临界区。这种解决方法依赖于进程执行的相对速度。
3.2.1 Dekker算法 • 3.第3种途径:两进程解法 (算法3) • 用一个布尔类型的数组flag • 数组元素初始化为 false; • flag[i] = false; • flag[j] = false; • 当进程 Pi要进入它的临界区,先置flag[i]为真 flag[i] = true; • 进入临界区前,Pi确信Pj没有准备进入临界区while(flag[j]==true){}; • 存在有限等待问题 • 两个进程可能同时设置它们的标志为 true ,那么就会在各自的 while 语句内无穷循环
3.2.1 Dekker算法 PROCESS 0 PROCESS 1 … … flag[0]: =true; flag[1]: =true; while flag[1] do {nothing}; while flag[0] do {nothing}; 〈critical section〉; 〈critical section〉; flag[0]: =false; flag[1]: =false; … … 这种方法保证了互斥但会导致死锁问题。
3.2.1 Dekker算法 4.第4种途径(接近正确,但还有缺陷;书上有错) PROCESS 0 PROCESS 1 … … flag[0]: =true; flag[1]: =true; while flag[1] dowhile flag[0] do beginbegin flag[0]: =false; flag[1]: =false; 〈delay for a short time〉; 〈delay for a short time〉; flag[0]: =true; flag[1]: =true; end; end; 〈critical section〉; 〈critical section〉; flag[0]: =false; flag[1]: =false; … …
3.2.1 Dekker算法 5.第五种途径(一个正确的解决方法) 用变量turn表示哪个进程坚持进入临界区 设计一个“指示”小屋,小屋内的黑板标明“turn”,当P0想进入其临界段时,置自己的flag为“true”,这时它去查看P1的flag,如果是“false”,则P0就立即进入自己的临界段,反之P0去查看“指示”小屋,如果turn=0,那么它知道自己应该坚持并不时去查看P1的小屋, P1将觉察到它应该放弃并在自己的黑板上写上“false”,以允许P0继续执行。 P0执行完临界段后,它将flag置为“false”以释放临界段,并且将turn置为1,将进入权交给P1。
3.2.2 Peterson算法 Dekker算法(正确的解法)可以解决互斥问题,但是,其复杂的程序难于理解,其正确性难于证明。Peterson给出了一个简单的方法。下面是一个两进程互斥的简单解决方法,进一步可将Peterson 算法推广到多个进程的情况。
3.2.2 Peterson算法 • 结合前面的算法 3 • 用flag数组标志和一个turn变量 • turn变量确保不会死锁 • 满足互斥,有空让进和有限等待的需求 • 仅解决两个进程的临界区问题 • 参见下面的代码 • (本书解决 n 个进程的临界区问题)
3.2.2 Peterson算法 For process i … flag[i] = true; // I want to go critical turn = j; // but you go first if you want while (flag[j] && turn == j) {}; // wait while j is incritical … critical section // go critical … flag[i] = false;// I’m done
3.2.2 Peterson算法 For process j … flag[j] = true; // I want to go critical turn = i; // but you go first if you want while (flag[i] && turn == i) {}; // wait while i is incritical … critical section // go critical … flag[j] = false;// I’m done
3.2.2 Peterson算法 var flag: array[0..1] of boolean;flag[1]: =true; turn: 0..1; turn: =0; procedure P0;while flag[0] and turn=0 do {nothing}; begin〈critical section〉; repeatflag[1]: =false; flag[0]: =true;〈remainder〉 turn: =1;forever while flag[1] and turn=1 do {nothing};end; 〈critical section〉;begin flag[0]: =false; flag[0]: =false; 〈remainder〉 flag[1]: =false; forever turn: =1; end;parbegin procedure P1; P0; P1 beginparend repeat end.
3.3 互斥——硬件解决方法 完全利用软件方法来解决进程互斥进入临界区的问题有一定的难度,且有很大局限性,现在有许多计算机提供了一些可以解决临界区问题的特殊的硬件指令。硬件方法通过特殊的机器指令实现互斥,可以降低开销。
3.3.1 禁止中断 在单处理机中,禁止进程被中断即可保证互斥,通过操作系统内核定义的禁止和允许中断的原语就可获得这种能力。进程执行临界段时不能被中断。 优点: • 在单处理机中可保证互斥。 缺点: • 代价较高,使执行效率显著降低 • 在多处理机系统中,禁止中断不能保证互斥
3.3.2 使用机器指令 1.特殊的机器指令 在多处理机系统中,多个处理机共享一个共同的主存,这里并没有主/从关系,也没有实现互斥的中断机制。许多系统都提供了一些特殊的硬件指令,允许我们在一个存储周期内去测试和修改一个字的内容(Test and Set指令),或者交换两个字的内容(Exchange指令)等等。这些特殊指令可以用来解决临界段问题。
3.3.2 使用机器指令 • 现代计算机系统可以支持像test-and-set和 swap 硬件指令;这些指令在一个指令周期内完成,是原子的 (即不可被中断)操作。 • 如果进程在进入临界区前首先必须运行如此的指令,那么可以达到互斥。 • 例如,如果两个 test-and-set指令同时执行(在不同的 CPU 上),那么它们会按任意顺序来执行。
3.3.2 使用机器指令 • 指令的定义 boolean testAndSet (boolean target) { boolean temp = target; target = true; return temp; } void swap (int *a, *b) { short temp = a; a = b; b = temp; }
3.3.2 使用机器指令 • 用 test-and-set 实现互斥 • … • while testAndSet(lock) {}; //entry section (busy waiting) • … • critical section • … • lock = false; //exit section • …
p1 lock false true false true p2 n y lock==T? lock==T? critical critical lock=false lock=false 3.3.2 使用机器指令 • test-and-set 同步
3.3.2 使用机器指令 • 用swap (全局的 lock,局部的 key) • … • key = true; • while (key == true) • swap(lock, key); //entry section (busy waiting) • … • critical section • … • lock = false; //exit section • …
swap slock,pkey swap slock,pkey pkey==T? pkey==T? critical critical slock=false pkey=true pkey=true pkey true false slock false true false true pkey true true false true p1 p2 y n n swap 同步
3.3.2 使用机器指令 2.机器指令方法的特性 优点: 可用于含有任意数量进程的单处理机或共享主存的多处理机; 比较简单,易于验证; 可支持多个临界段,每个临界段用各自的变量加以定义。 缺点: 采用busy-waiting技术,进程等待进入临界段时耗费处理机时间; 可能产生饥饿(参见P87); 可能产生死锁(参见P87) 。
3.4 信号量 信号量是一个由整型变量和对应的队列所组成的特殊变量,除对其初始化外,它只可以由两个不可中断的P(Wait)、V(Signal)操作存取。不论是采用一般信号量还是二元信号量,进程都将排队等候信号量,但这并不意味着进程移出的顺序与队列次序相同。 基本原则: 两个或多个进程可通过单一的信号量展开合作,即进程在某一特定的地方停止执行,直到某个特定的信号量到来为止。通过信号量,任何复杂的合作要求都可被满足。
3.4 信号量 • 定义 wait和 signal函数 • 当 wait 被调用时如果信号量在使用中其值<0,则进程用 block 操作阻塞自己,也就是进程把自己放到与信号量相关的等待队列中。 • 当signal被调用时,唤醒下一个正等待该信号量的进程。
3.4 信号量 • 信号量原语的定义 void wait(semaphore S) { S.value--; if (S.value < 0) { add process to S.L; block(); } } void signal(semaphore S) { S.value++; if (S.value <= 0) { remove P from S.L; wakeup(P); } }
3.4 信号量 • 二元信号量原语的定义 其信号量的取值只能是 0 或 1,一般用于互斥操作。 void waitB(semaphore S) { if (S.value ==1) S.value = 0; else { add process to S.L; block(); } } void signalB(semaphore S) { if (S.L== NULL) S.value = 1 else { remove P from S.L; wakeup(P); } }
3.4.1 用信号量解决互斥问题 信号量的互斥算法可以用小屋模型来描述。除了黑板外,小屋中还有一个大冰箱。某进程进入小屋后执行 wait 操作将黑板上的数减1,这时,如果黑板上的值非负,它就进入临界段,反之它就进入冰箱内冬眠。这时,就允许另一进程进入小屋。当一个进程完成其临界段后,它进入小屋执行 signal,将黑板上的值加1,这时如果黑板上的值为非正数,它就从冰箱中唤醒一个进程。
3.4.1 用信号量解决互斥问题 Program mutualexclusion;begin ( * main program * ) Count=n; (* number of processes *);parbegin Var s:semaphore(:=1);P(1); Procedure P(i:integer); P(2); Begin …… ……P(n); Repeat parend Wait(s); end. ( critical section ); Signal(s); ( remainder ) …… Forever End; 注意:信号量解决互斥与软件、硬件解决的根本差别在那里?
p1 p2 p3 semaphore s 1 0 -1 -2 -1 0 1 critical wait(s) critical wait(s) signal(s) signal(s) critical signal(s) wait(s) Wait 操作信号量非负,P1进入临界区 Wait 操作信号量为负,P2阻塞 Wait 操作信号量为负,P3 阻塞 Signal 操作后信号量非正,从等待队列中唤醒一个进程 Signal 操作后信号量非正,从等待队列中唤醒一个进程 Signal 操作后信号量为正,表示已无进程在临界区 用信号量实现互斥
例:设余票单元为 count,有 N 个售票进程正确并发执行的算法 Process Pi(i=1,2,…n) begin Begin s:semaphore; Var Xi:integer; s:=1;count:integer; 按旅客要求找到 count; parbegin Wait(s); P1; Xi:=count; P2; If Xi>=1 then …… begin Pn; Xi:=Xi-1; parend count:=Xi; end. Signal(s); 输出一张票; End Else Begin Signal(s); 输出“票已售完”; End; End;
如果改成下面这种写法行吗?为什么? Process Pi(i=1,2,…n) begin Begin s:semaphore; count:integer; Var Xi:integer; s:=1; 按旅客要求找到 count; parbegin Wait(s); P1; Xi:=count; P2; If Xi>=1 then …… begin Pn; Xi:=Xi-1; parend Xi:=count; end. Signal(s); 输出一张票; End Else 输出“票已售完”; End;
3.4.2 用信号量解决生产者/消费者问题 问题描述如下: 一个或更多的生产者生产出某种类型的数据(记录、字符),并把它们送入缓冲区,惟一的一个消费者一次从缓冲区中取走一个数据,系统要保证缓冲区操作不发生重叠,即在任一时刻只能有一方(生产者或消费者)访问缓冲区。
3.4.2 用信号量解决生产者/消费者问题 • 这是一个进程同步问题 生产者进程在送数据前要确定缓冲区不满,把一个数据送入缓冲区后要发信号给消费者;同样消费者在取数据前要确定缓冲区不空,从缓冲区取一个数据后要发信号给生产者。 • 如果进程间不能同步(相互制约),那么可能导致数据被覆盖或重复取数或取无用的数据(随机数)。
3.4.2 用信号量解决生产者/消费者问题 • 假如是一个生产者、一个消费者,且缓冲区只有一个单元。 Begin Process producerProcess consumer Buffer:integer; BeginBegin Se,Sf:semaphore; RepeatRepeat Se:=1;Sf:=0; Produce a product;Wait(Sf); parbegin Wait(Se);Take a product; producer; Buffer:=product;Signal(Se); consumer; Signal(Sf);Consumer; parend Forever Forever End. End;End; 验证:1、缓冲区满生产者不能送数;缓冲区空消费者不能取数 2、在这个问题中同步隐含着互斥。
3.4.2 用信号量解决生产者/消费者问题 • 假如是一个生产者、一个消费者,且缓冲区有 N 个单元。 • 当缓冲区没有放满 N 个数据时,生产者进程调用 wait(Se)都不会成为等待状态,可以把数据送入缓冲区。但当缓冲区中已有 N 个数据时,生产者进程想要再送数将被拒绝。由于每送入一个数后要调用 Signal(Sf) ,所以此时 Sf 的值表示缓冲区中可取的数据数,只要 Sf ≠ 0,消费者进程在调用 wait(Sf) 后总可以从缓冲区取数。每取走一个数据就调用 Signal(Se),因此增加了一个存放数据的位置。可以用指针 in 和 out 分别指示生产者进程向缓冲区送数和消费者进程从缓冲区取数的相对位置;指针的初始值为“0”。 • 这种情况下生产者与消费者进程的同步算法为:
3.4.2 用信号量解决生产者/消费者问题 Begin Process producerProcess consumer B:array[0..n-1] of integer; Begin Begin in,out:integer; RepeatRepeat Se,Sf:semaphore; Produce a product;Wait(Sf); in:=out:=0; Wait(Se);Take a product from B[out]; Se:=n;Sf:=0; B[in]:=product;out:=(out+1) mod n; parbegin in:=(in+1) mod n;Signal(Se); producer; Signal(Sf);Consumer; consumer; Forever Forever parend End;End; End. 说明:这时缓冲区可以看成是头尾相连一个环形,in、out 指针指出存取数的位置。 验证:1、缓冲区满生产者不能送数;缓冲区空消费者不能取数 2、在这个问题中生产者与消费者进程有可能同时进入缓冲区,但不会出错。
3.4.2 用信号量解决生产者/消费者问题 • 假设是M个生产者、R个消费者,且缓冲区有 N 个单元。 • 现在不仅生产者与消费者之间要同步,而且 M 个生产者之间、R 个消费者之间还必须互斥地访问缓冲区。 • 要同步的原因和前面的问题一样;之所以要互斥是因为如果 M 个生产者进程各自个向缓冲区送数,当第一个生产者按指针 in 指示的位置送一个数,但在改变指针前可能被打断执行,于是当第二个生产者送数时仍按原指针所指出的位置存放,这样两个数被放在同一个单元,造成数据丢失。同样,R 个消费者都要取数时可能都从指针 out 指出的位置取数,造成一个数据被重复取出。
3.4.2 用信号量解决生产者/消费者问题 • 按课本的问题描述在任一时刻只能有一方(生产者或消费者)访问缓冲区。 即一次只能有一个进程可以进入缓冲区。这是第一种方法。 • 可是、当有一个生产者(或消费者)在送数(或取数)时,可以允许一个消费者(或生产者)同时访问缓冲区去取数(或送数)。因此这是第二种方法。 • 显然、第二种方法的并行性要比第一种方法的并行性高。
3.4.2 用信号量解决生产者/消费者问题 • 第一种方法 Begin Process producer i Process consumer j B:array[0..n-1] of integer; Begin Begin in,out:integer; RepeatRepeat S,Se,Sf:semaphore; Produce a product;Wait(Sf); in:=out:=0;Wait(Se) Wait(S); S:=1;Se:=n;Sf:=0 Wait(S);Take a product from B[out]; parbegin B[in]:=product; out:=(out+1) mod n; producer; in:=(in+1) mod n;Signal(Se); consumer; Signal(Sf);Signal(S); parend Signal(S);Consumer; End. Forever Forever End;End; • 思考:1、如果生产者或消费者进程中的两个 Wait 操作次序对调 可以吗? 2、第二种方法应该如何改写?
3.4.2 用信号量解决生产者/消费者问题 用二元信号量来解决此问题*: 在任何时候生产者(P)都可向缓冲区中添加数据,在添加数据前,P执行waitB(s),然后执行signalB(s)以防止在添加过程中,别的消费者(C)或P访问缓冲区。在进入到临界段时,P将增加n的值,如果n=1则在此次添加数据前缓冲区为空,于是P执行signalB(delay)并将这个情况通知C。C最初执行waitB(delay)来等待P生产出第一个数据,然后取走数据并在临界段中减小n的值。如果P总保持在C前面,那么C就不会因为信号量delay而阻塞,因为n总是正数,这样P和C都能顺利地工作。这个方法也存在缺陷有可能导致死锁。 解决这个问题的一个方法是引入一个附加变量,它在消费者的临界段中设置。这样,就不会出现死锁了。
3.4.3 信号量的实现 wait和signal操作都必须作为原子操作实现。显然,用硬件方法或固件方法都可解决这一问题,而且还有其他解决方法。尽管wait和signal操作执行的时间较短,但因包含了忙--等,故忙--等占用的时间是主要的(见图3.15a)。 对单处理机系统而言,可以在wait和signal操作期间屏蔽中断,而且这些操作的执行时间相对较短(见图3.15b) 。 固件注:现在硬件与软件间的界限越来越模糊,许多原来属于软件的功 能,通过程序设计技术可以转化为硬件,即所谓的固化;因此 具有软件功能的硬件称为固件。