本章讲述在并发处理中通常需要解决的两个问题:死锁和饥饿
处理死锁的三种常用方法:预防、检测和避免
经典问题:哲学家就餐问题
一、死锁原理
可以把死锁定义为一组互相竞争系统资源或进行通信的进程间的“永久”阻塞。
当一组进程中的每个进程都在等待某个事件(典型的情况是等待所请求的资源被释放),而只有在这组进程中的其他被阻塞的进程才可以触发该事件,这时就称这组进程发生死锁。因为没有事件能够被触发,故死锁是永久性的。死锁问题并没有一种有效的通用解决方案。所有死锁都涉及两个或多个进程之间对资源需求的冲突。资源通常可分为两类:可重用资源和可消耗资源。
1 可重用资源
可重用资源是指一次只能供一个进程安全地使用,而且不会由于使用而耗尽的资源。进程得到资源单元,后来又释放这些单元,供其他进程再次使用。
可重用资源的例子包括处理器、IO通道、内存和外存、设备,以及诸如文件、数据库和信号量之类的数据结构。
2 可消耗资源
可消耗资源师指可以被创建(生产)和销毁(消耗)的资源。通常对某周类型可消耗资源的数目没有限制,一个无阻塞的生成进程可以创建任意数目的这类资源。当消费进程得到一个资源时,该资源就不存在了。
可消耗资源的例子有中断、信号、消息和IO缓冲区中的信息。
3 死锁的条件
死锁的三个必要条件:
1)互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源;
2)占有且等待:当一个进程等待其他进程时,继续占有已经分配的资源;
3) 不可抢占:不能强行抢占进程已占有的资源。
对死锁的产生,还需要第四个条件:
4)循环等待:存在一个封闭的进程链,使得每个进程至少占有此链中下一个进程所需要的一个资源。
第四个条件实际上是前三个条件的潜在结果。假设前三个条件存在,可能发生的一系列事件会导致不可解的循环等待。
二、死锁预防
死锁预防策略是试图设计一种系统来排除发生死锁的可能性。可以把死锁预防方法分成两类。一种是间接的死锁预防方法,即防止前面勒出的桑必要条件中任何一个的发生;一种是直接的死锁预防方法,即防止循环等待的发生。
三、死锁避免
死锁避免允许三个必要条件,但通过明智的选择,确保永远不会到达死锁点,因此死锁避免比死锁预防允许更多的并发。
在死锁避免中,是否允许当前的资源分配请求时通过判断该请求是否可能导致死锁来决定的。因此,死锁避免需要知道将来的进程资源请求的情况。
两种死锁避免方法:
1)如果一个进程的请求会导致死锁,则不启用此进程;
2)如果一个进程增加的资源请求会导致死锁,则不允许此分配。
死锁避免的优点是它不需要死锁预防中的抢占和回滚进程,并且比死锁预防的限制少。
四、死锁检测
死锁预防策略是非常保守的,他们通过限制访问资源和在进程上加强约束来解决死锁的问题。
死锁检测策略则完全相反,它不限制资源访问或约束进程行为。对于死锁检测来说,只要有可能,被请求的资源就被分配给进程。操作系统周期性的执行一个算法检测前面的条件。
五、哲学家就餐问题
算法必须保证互斥(没有两位哲学家同时使用一把叉子),同时还要避免死锁和饥饿。
哲学家就餐问题可以视为当应用程序中包含并发线程的执行时,协调处理共享资源的一个有代表性的问题。
1 基于管程的解决方案:
管程包含了两个过程。get_forks函数用于表示哲学家取他左边和右边的叉子。如果至少有一把叉子不可用,那么哲学家进程就会在条件变量的队伍中等待。这可让另外的哲学家进入这个管程。release-forks函数用来标志两把叉子可用。
六、UNIX的并发机制
UNIX为进程间的通信和同步提供了各种机制。这里,最几种的包括:
管道,消息,共享内存,信号量,信号。
1)管道:管道在创建时获得一个固定大小的字节数。当一个进程试图往管道中写时,如果有足够的空间,则写请求被立即执行;否则该进程被阻塞。类似的,如果一个读进程试图读取的字节数多于当前管道中的字节数时,它也被阻塞;否则读请求被立即执行。操作系统强制实施互斥,即一次只能有一个进程可以访问管道。
2)共享内存:共享内存是UNIX提供的进程间通信手段中速度最快的一种。
3)信号:信号时用于向一个进程通知发生异步事件的机制。信号类似于硬件中断,但没有优先级,即内核平等的对待所有的信号。
七、Linux内核并发机制
Linux包含了在其他UNIX系统中出现的所有并发机制,其中包括管道、消息、共享内存和信号。Linux还包含一套丰富的并发机制,这套机制是特别为内核态线程准备的。换言之,他们是用在内核中的并非机制,提供内核代码执行中的并发性。
1 原子操作
Linux提供了一组操作以保证对变量的原子操作。这些操作能够用来避免简单的竞争条件。原子操作执行时不会被打断或被干涉。在单处理器上,线程一旦启动原子操作,则从操作开始到结束这段时间内,线程不能被中断。此外,在多处理器系统中,该原子操作所针对的变量时被锁住的,以免被其他的进程访问,知道此原子操作执行完毕。
2 自旋锁
在Linux中保护临界区最常见的技术是自旋锁。在同一时刻,只有一个线程能获得自旋锁。其他任何企图获得自旋锁的线程将一直进行尝试,直到获得了该锁。
本质上,自旋锁建立在内存区中的一个整数上,任何线程进入临界区之前都必须检查该整数。如果该值为0,则线程设置该值为1,然后进入临界区。如果该值非0,则该线程继续检查该值,直到它为0。
自旋锁很容易实现,但有一个缺点,即在锁外面的线程以忙等待的方式继续执行。
使用自旋锁的基本形式如下:
spin_lock(&lock)
/*临界区*/
spin_unlock(&lock)
八、小结
死锁是指一组征用系统资源或互相通信的进程被阻塞的现象。这种阻塞是永久的,除非操作系统采取某些非常的行动(杀死或回滚进程)。死锁可能涉及可重用资源或可消耗资源。
处理死锁通常有三种方法:预防、检测盒避免。死锁预防通过确保死锁的一个必要条件不会满足,保证不会发生死锁。如果操作系统总是同意资源请求,则需要进行死锁检测。操作系统必须周期性的检查死锁,并采取行动打破死锁。死锁避免设计分析新的资源请求,以确定它是否会导致死锁,并且只有当不可能发生死锁时才同意该请求。