本文主要讲解多线程(进程)编程时出现竞争条件的解决方案及代码实现。
竞争条件
协同进程可能共享一些彼此都能够读写的公用存储区(例如打印机脱机系统),也可能是一些共享文件,当两个或多个进程读写某些共享数据,而最后的结果却绝育进程运行的精确时序,就称为竞争条件(race condition)。
如果程序之间有竞争条件,也许大部分的运行结果都很好,但在极少数情况下会发生一些难以解释的事情。
互斥(mutual exclusion)
要避免这种错误,关键是要找出某种途径防止多个进程在使用一个共享变量或文件时,其他进程不能做同样的事。
互斥的实现有很多种,在UNIX编程中,总体来说有两种大的方案:
- 忙等待形式的互斥
- 优势在于被等待的进程(线程)不需要context switch(no extra overhead),提高了效率
- 但若等待时间较长,会浪费CPU资源。
- 会造成优先级反转问题(priority inversion problem)
- 睡眠等待
- CPU利用率较高,但会造成context switch的overhead。
临界区
把对共享内存进行访问的程序片段称为临界区或者临界段(critical region)。
如果能够进行适当安排,使得两个进程不可能同时处于临界区,则能够避免竞争条件。
我们认为一个好的方案应该能解决竞争条件的同时,依然高效地进行操作,满足以下四个条件:
- 任何两个进程不能同时处于临界区。
- 不应该对CPU的速度和树木做任何假设。
- 临界区外的进程不得阻塞其他进程。
- 不能使进程在临界区外无休止的等待。
忙等待的互斥
关闭中断
这是最简单也最直接的方案,使得每个进程在进入临界区后先关闭中断,在离开之前再打开中断。
中断被关闭后,时钟中断也会关闭。因此CPU在做完临界区之前都不会发生进程切换。
缺点:
- 把关闭中断的权利交给用户进程是不明智的。可能会造成系统终止。
- 不适用于多CPU情形。
- 在实际中很少采用。
关闭中断对于操作系统是一项很有用的技术,但对于用户进程不是一种合适的通用互斥机制。
锁变量
设想有一个共享锁变量,在进程想要进入临界区时,先测试这把锁。
但可以想象,如果锁变量依然是普通类型(不是原子类型),则依然会发生竞争条件。
严格交替法
首先看示意代码:
/// process 0 ////
while(true){
while(turn!=0);
critical_region();
turn=1;
noncritical_region();
}
/// process 1 ////
while(true){
while(turn!=1);
critical_region();
turn=0;
noncritical_region();
}
process 0 必须在 turn 变量等于0时才会进入临界区,process 1 必须在 turn 变量等于1时才能进入临界区。
假设turn变量初始化为0,则process 1会一直持续地检测一个变量,直到为1才执行下面的代码。这种等待为忙等待。一个适用于忙等待的锁称为自旋锁(spin lock)。
仔细观察以上代码,两个进程互相依靠对方提供的turn变量才能继续下去。若一个进程的noncrit