Race Condition 竞态条件。
避免竞态条件:原子操作。
原子操作是指一次不存在任何中断或者失败的执行。该执行成功结束,或者根本没有执行,并且不应该发现任何部分执行的状态
临界区,是指进程中的一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会被执行的代码区域。
互斥,当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并且访问任何相同的共享资源。
死锁:两个或以上的进程,在相互等待完成特定任务,而最终没法将自身任务进行下去。
饥饿:一个可执行的进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行。
临界区的特征:
互斥:同一时间临界区中最多存在一个线程;
Progress:如果一个线程想要进入临界区,那么它最终会成功;
有限等待:如果一个线程 i 处于入口区,那么在 i 的请求被接受之前,其他进程进入临界区的时间是有限制的 ;
无忙等待(可选):如果一个进程在等待进入临界区,那么它可以进入之前会被挂起。
三种方法:禁用硬件中断;基于软件的方法;更高级抽象(原子操作指令)
1.屏蔽硬件中断:
没有中断,没有上下文切换,因此没有并发。硬件将中断处理延迟到中断被启用之后。
进入临界区,禁用中断。离开临界区,开启中断。
缺点:一旦中断被禁用,线程就无法被停止。整个系统都会为你停下来,可能导致其他线程处于饥饿状态。
要是临界区任意长,无法限制响应中断所需的时间。
如果一个进程是由两个CPU并行执行,那么对另一个CPU不起作用,所以中断的方法是有限制的,在多CPU情况下无法解决互斥。
2.基于软件的解决方法:
假设两个线程,T0,T1
Ti的通常结构:
do{
进入区域
临界区
离开区域
提醒区域
}while(1)
Peterson算法:
使用两个共享数据项
int turn;//指示该谁进入临界区
bool flag[];//指示进程是否准备好进入临界区
CODE FOR ENTER_CRITICAL_SECTION
flag[i] = TRUE;
turn = j;
while(flag[j] && turn == j);
CODE FOR EXIT_CRITICAL_SECTION
flag[i] = FALSE;
进程Pi的算法:
do{
flag[i] = TRUE;
turn = j;
while(flag[j] && turn ==j);
CRITICAL SECTION
flag[i] = FALSE;
REMAINDER SECTION
}while(TRUE);
还有个Dekker算法,比Peterson复杂些,但用到的变量也是一样的。
针对N进程的话:
有一个Bakery算法
N个进程的临界区
进入临界区之前,进程接收一个数字
得到的数字最小的进入临界区
如果进程Pi和Pj收到相同的数字,那么如果 i<j ,Pi先进入临界区,否则 Pj先进入临界区
编号方案总是按照枚举的增加顺序生成数字
没有硬件保证的情况下,无真正的软件解决方法。Peterson算法需要原子的LOAD和STORE指令。
3.基于硬件的原子操作的高层抽象实现:
硬件提供了一些原语,像中断禁用,原子操作指令等。
锁是一个抽象的数据结构:
一个二进制状态(锁定/解锁),两种方法
Lock::Acquire() - 锁被释放前一直等待,然后得到锁。
Lock::Release() - 释放锁,唤醒任何等待的进程
使用锁来编写临界区
lock_next_pid->Acquire();
new_pid = next_pid++;
lock_next_pid->Release();
大多数现代体系结构都提供特殊的原子操作指令,通过特殊的内存访问电路,针对单处理器和多处理器
两个原子操作指令:
Test-and-Set 测试和置位,完成的事情:
从内存中读取值;测试该值是否为1(然后返回真或假);内存值设置为1
交换 Exchange 交换内存中的两个值
他两的语义:
bool TestAndSet(bool* target)
{
bool rv = *target;
*target = TRUE;
return rv;
}
void Exchange(bool*a , bool* b)
{
bool temp = *a;
*a = *b;
*b = temp;
}
实现:
class Lock{
int value = 0;
}
Lock::Acquire(){
while(test-and-set(value));//spin
}
如果锁被释放,那么test-and-set读取0并将值设置为1,锁被设置为忙并且需要等待完成。
如果锁处于忙状态,那么test-and-set读取1并将值设置为1,不改变锁的状态并且需要循环(自旋spin)
Lock::Release(){
value = 0;
}
while循环是个忙等,线程在等待的时候消耗CPU周期。改进:
无忙等待:
class Lock{
int value = 0;
WaitQueue q;
}
Lock::Acquire(){
while(test-and-set(value)){
add this TCB to wait queue q;
schedule();
}
}
Lock::Release(){
value = 0;
remove one thread t from q;
wakeup(t);
}
用Exchange完成进入临界区和退出临界区的实现
共享数据(初始化为0) int lock = 0;
线程Ti
int key;
do{
key = 1;
while(key == 1) exchange(lock,key);
critical section
lock = 0;
remainder section
}
基于原子操作的互斥实现优点:适用于单处理器或者共享主存的多处理器中任意数量的进程;简单且容易证明;可以用于支持多临界区
缺点:忙等待消耗处理器时间;当进程离开临界区并且多个进程在等待的时候可能导致饥饿;死锁,如果一个低优先级的进程拥有临界区并且一个高优先级进程也需求,那么高优先级进程会获得处理器并等待临界区
可以通过锁来实现进入临界区和退出临界区的方式,锁是更高等级的编程抽象
互斥可以使用锁来实现;通常需要一定等级的硬件支持