多线程并发会导致资源竞争。
同步即协调多线程对共享数据的访问,保证任何时刻都只能有一个线程执行临界区代码。
互斥是解决进程同步的方式。
独立进程是指一个进程在运行过程中不会与其他进程进行交互的进程。
但是,进程在运行的过程中不可避免的要进行进程间合作。
进程合作的优势:
进程间合作要避免不确定性和不可重现性。由于进程调度的不确定性,因此进程的一个操作可能被打断。
进程间通信要解决三个问题:
竞争条件是说多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
案例:
原子操作是指一次不存在任何中断或者失败的执行。
但是实际上操作往往不是原子的
操作系统需要利用同步机制在进程并发执行的同时保证一些操作是原子操作。
临界区是指多个程序访问临界资源的一段代码。也可以说是进程访问临界资源的一段需要互斥的代码。
要避免竞争条件,即要找出某个途径来阻止多个进程同时读写共享数据,提出一种解决方案为互斥,即以某种手段保证当一个程序在使用一个共享爱那个文件或变量时,其他进程不能做同样的操作。
互斥是指当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并访问任何相同的共享资源。
死锁是指多个进程相互等待完成特定任务二最终无法将自身任务进行下去。
饥饿是指一个可执行的进程被调度器持续忽略,以至于虽然处于可执行状态却不被执行。
避免竞争条件即使临界区中只能有一个进程。
一个好的解决竞争条件的方案需要满足下列四个条件:
连续测试一个变量直到某个值出现为止称为忙等待,用于忙等待的锁称为自旋锁。
下列几种方式都可以保证任意两个进程不同时处于临界区。
单处理器系统中,最简单的方法是使每个进程刚进入临界区后就立即屏蔽所有中断,并在就要离开之前再打开中断。屏蔽中断后,时钟中断也会被屏蔽,而CPU只有发生时钟中断或其他中断时才会进行进程切换,因此在屏蔽中断之后就不会被切换到其他进程。
但是这种方式有一个问题:若此进程不打开中断的话其他进程就都无法执行。
而且若是处于多处理器系统中,其他的CPU也可以将进程调度,从而还是无法避免多个进程同时访问临界区的问题。
因此,屏蔽中断对用户进程不是一种合适的通用互斥机制。
总结如下:
缺点:
通过共享一些共有变量来同步进程的行为。
turn用来表示哪一个进程进入临界区,假设只有两个进程,进程0执行a代码,进程1执行b代码,此时考虑两种情况:
由此可见,此种方式也不合适。
此算法可以解决两个进程的同步问题。
进程Pi要等待从turn到i-1的进程都退出临界区后访问临界区。
进程Pi退出时将turn给下一个想要进入的进程Pj。
硬件提供了一些同步原语
操作系统提供更高级的编程抽象来监护进程同步
现代CPU体系结构都提供一些特殊的原子操作指令。
测试和置位(Test-and-set)指令:
交换(exchange)指令:
锁是一个抽象的数据结构
使用锁来控制临界区的访问:当进程进入临界区之前先判断锁变量的值,若有锁的话就等待,否则进入临界区并且更改锁变量的值,然后再退出临界区时将锁变量的值再次修改。
但是这又回到了最开始的问题:读锁变量和修改锁变量时两个操作,在第一个进程读取锁变量时发现可以进入,然后CPU调度第二个进程进入CPU进行执行,此时第一个进程还未将锁变量进行修改,因此第二个进程也发现可以进入,因此更改了锁变量进入临界区,此时又被切换至第一个进程,第一个进程认为此时临界区中没有进程,因此也进入了临界区,所以还是不能解决竞争条件的问题。
//忙等待,此种方式为自旋锁方式
class Lock {
int value = 0;
}
Lock::Acquire() { //若锁被释放,则TS指令读取0并将值设置为1,否则,TS指令读取1并将值设置为1
while(test-and-set(value))
;
}
Lock::Release() {
value = 0;
}
//无忙等待,此种方式为等待队列方式,即阻塞式方法。
class Lock {
int value = 0;
WaitQueue q;
}
Lock::Acquire() {
while(test-and-set(value)) {
//将当前进程加入等待队列
add this PCB to wait queue q
//执行调度
schedule();
}
}
Lock::Release() {
value = 0;
//从等待队列中移除一个进程p,并唤醒他去执行。
remove one process p from q;
wakeup(p);
}
优点:
缺点:
清华大学—操作系统原理
现代操作系统