摘要:
在对多线程并发的编程环境下,死锁是我们经常碰到的和经常需要解决的问题。所谓死锁,即:由于资源占用是互斥的,当某个线(进)程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁,如下图:
线程#1在获得Lock A后,需要获得Lock B,而同时,线程#2在Lock B后,需要获得Lock A。对于线程#1和#2,由于都不能获得满足的条件,而无法继续执行,死锁就形成了。
死锁是多线程并发编程的大难题,我们可以通过Log Trace、多线程编程辅助工具、IDE调试环境等手段进行调试、跟踪。然而,另一个更难对付的问题是“假死锁”(我在这里暂且称为“假死锁”,实在找不到什么更好的称呼)。所谓的假死锁,我给出的定义是:在有限的时间内的死锁。与死锁不同的是,其持续的时间是有限的,而大家都知道,死锁持续的时间是无限的,如果碰到死锁,程序接下来是什么都干不了了。而正是由于假死锁的相对的持续时间,给我们编程人员会带来更大的麻烦。可以想象得到,我们想通过某些工具来Trace这样一个特定的时间段是非常困难的,更多的情况下,我们需要结合LOG进行合理的分析,使得问题得以解决。本文就假死锁产生的条件,环境,以及解决的办法做一个讨论。
一、假死锁的产生条件。
考虑下面的例子(我只是给给出了伪代码),假设我们系统中的线程个数是确定的,有限的。在本例中,系统总的线程数目是3。如下图:
线程#1,#2,#3都可能被调度进入临界区A,我们假设线程#1执行临界区A时花费了10s的时间,而在这10s的时间里,线程#2与线程#3都处于等待的状态。也就是说:在这个10s的时间里,系统是没法响应任何的其他请求。我们称之为10s的假死锁。如果在这段时间里,系统需要一些关键的请求被执行,这些关键请求是需要real time地被处理,比如说是Timer事件,则后果是不堪设想的。(注意:我们的假定是系统中的线程只有#1,#2,#3)。
以此,总结一下发生假死锁的条件,如下:
--〉临界区的代码在集中的时间段内,可能被系统中的任意线程执行,完全由操作系统决定。
--〉临界区的代码在某些情况下,可能是很耗时的。(比如:其执行时间大于100ms,或者,甚至是秒级别的)
二、在Proactor(IOCP)中的假死锁。
在前面的文章中,我提到过在windows平台上,Proactor设计模式是基于IOCP的。在这里,本文不会用过多的语言来阐述Proactor是怎样的设计,重点放在Proactor的假死锁及其一些解决的办法。另外需要说明的是,我这里所说的Proactor,在技术层面上,等同于IOCP,我们也可以按照IOCP来理解我所阐释的概念。
我们都知道,IOCP是靠工作者线程来驱动的。工作者线程与一个完成端口对象相关联,当IO 请求被投递到完成端口对象时,这些线程为完成端口服务。需要说明的是,应该创建多少个线程来为完成端口服务,是你的应用设计来决定的(很重要的的一点是:在调用CreateIoCompletionPort时指定的并发线程的个数,和创建的工作者线程的个数是有区别的,详细的技术细节,请参考其他资料)。但是总的来说,在你的系统交付运行后,工作者线程的线程数目是一个确定的值。其结构图,大致如下:
我们假定使用了线程数目为4的工作者线程来为完成端口服务,它们通过调用来GetQueuedCompletionStatus方法来从完成端口中获取IO相关的packet,一旦获得,它们都会回调业务逻辑层的代码来进行相关的业务逻辑处理。到这里我们看到,假设,在业务逻辑层存在临界互斥区,并且在某一个集中的时间段内,工作者线程都可能被调度执行该临界互斥区,那么,假死锁的条件基本形成,如果某一个线程在该区域花费的时间比较长,假死锁就会发生。
一般来说,解决这样的问题的关键就是打破形成假死锁的条件:
第一、在回调函数里,尽量减少锁的使用。
第二、减量减少临界互斥区的执行时间。对于一些慢速的操作尤其注意。比如:当你在临界互斥区访问慢速的IO操作时(打开文件,读写文件等),可能需要考虑Cache机制,通过使用内存来代替慢速的disk。
第三、将临界互斥区代码委托给另外独立的线程(或线程组)执行,代价是增加这些线程间的通讯。
第四、通过使用流控等手段,避免让所有的线程在集中的时间段内访问该临界互斥区。
三、结束语:
事实上,类似这样的问题,一旦存在,是很难发现和调试的。不过对于多线程的编程,我们都应该遵守以下的基本原则,以最大化的防止死锁和假死锁的发生。
--> 尽量减少锁的使用频率和保护范围。
--> 当线程在互斥锁的保护范围内执行代码时,应该:尽量减少对慢速IO设备的访问(如:disk),尽量避免获得其它互斥资源。
--〉正确使用各种锁,包括:原子操作原语,Read Lock, Write Lock, 和Recursive Lock等。这些锁在不同的场景下有着不同的作用。