死锁是进程死锁的简称,是由Dijkstra于1965年研究银行家算法时首先提出来的。它是计算机系统乃至并发程序设计中最难处理的问题之一。实际上,死锁问题不仅在计算机系统中存在,在我们日常生活中它也广泛存在。这里,我想抛开语言,用一个例子来聊一聊死锁问题,聊一聊它是怎么产生的?应该怎么防止?
假设我们有一把蓝钥匙,可以打开一扇蓝色的门;以及一把红钥匙,可以打开一扇红色的门。两把钥匙被保存在一个皮箱里。同时我们定义六种行为:获取蓝钥匙,打开蓝色门,归还蓝钥匙,获取红钥匙,打开红色门,归还红钥匙。
游戏规则是:一个人(线程)必须通过排列六种指令的顺序,打开两扇门,最后归还钥匙。
随便排一下就好了啊,只要注意同一颜色的三种指令的先后顺序是“取钥匙 -> 开门 -> 还钥匙”就可以了。比如下面这种执行顺序:
计算下我们可以知道,可能的解法一共有20种( ),这里画出其中的6种:
同样的规则,只不过换成两个线程同时进行这个游戏,每个线程都有各自的两扇门需要打开,但是两个线程共享一副红蓝钥匙。如果某个线程取钥匙时发现钥匙已被另一个线程取走了,它会等着,等到另一个线程归还了钥匙之后再继续。
我们从单人模式的6组解中随机全取两组:解法3和解法4。
把这两组解分别作为线程A和线程B,并且同时开始执行这两组指令:
由于线程B在第四步的时候,需要等线程A运行到第六步归还了蓝钥匙之后才能继续,所以最后线程A先跑完,线程B后跑完,但是两个线程都成功的完成了任务。
那是不是从6组解法中任意选出两组解法都可以呢?
我们这次选解法3和解法5作为线程A和线程B试一试:
同时开始运行线程A和线程B:
可以看到,当两个线程都运行到第三步的时候,线程A在等线程B归还红钥匙,线程B在等线程A归还蓝钥匙,因而两个线程都永远卡在那里无法前进。
这就是形成了死锁。
在全部的20种可能线程解法中,随机选出两组线程,有一定的几率会出现死锁的情况(有兴趣的知友可以算一算这个几率是多少?如果是选三组线程呢?)。一般来说死锁的出现必须满足以下四个必要条件:
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
——只有一副钥匙
请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
——拿着红钥匙的人在没有归还红钥匙的情况下,又提出要蓝钥匙
不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
——人除非归还了钥匙,不然一直占用着钥匙
环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
——拿着红钥匙的人在等蓝钥匙,同时那个拿着蓝钥匙的人在等红钥匙
要避免出现死锁的问题,只需要破坏四个条件中的任何一个就可以了。
我们来看看常见的避免死锁的方法。
只有一副钥匙,这是形成死锁的最关键的原因。显然,如果我们能在两个线程跑之前,能给每个线程单独拷贝一份钥匙的副本,就能有效的避免死锁了。
当然,这种方法试用范围并不广。因为有时如果系统拷贝那副钥匙的成本极高,而线程又很多的话,这种方法就不适用了。
会出现死锁的两两组合,一定都是一个线程先取了红钥匙而另一个线程先取了蓝钥匙,从而导致了可能形成了“环路等待”。所以我们可以强制规定任何线程取钥匙的顺序只能是 “先取蓝钥匙再取红钥匙”的话,就能避免死锁了。(六组解也就只剩下前三组解是有效的了)
除非线程自己还钥匙,否则线程会一直占有钥匙,是形成不可剥夺条件的原因。这里,我们可以通过设置一个”最长占用时间“的阈值来解决这个问题——如果过了10分钟仍然没有进入下一个步骤,则归还已有的钥匙。这样的话,两个线程都能取到所需的钥匙继续下去了。
任何一个线程“贪心”,都可能会导致死锁。大致就是说有了一把钥匙还没还就要另一把。这里我们可以通过规定在任何情况下,一个线程获取一把钥匙之后,必须归还了钥匙之后才能请求另一把钥匙,就可以有效解决这个问题。
我经常用这个取钥匙开门的例子来讲解死锁,一个原因是讲解的过程中可以用人和实物来亲自示范,比较容易给人留下印象。更重要的一点是,这个例子对应的几种避免死锁的方法和思想可以很容易地运用到实际的编程当中。
死锁是一个很有意思的话题,实际工作中接触到的频率还蛮高的。所有的死锁问题中,SQL数据库的死锁问题其实是被讨论的最多的。下一次有机会专门来聊一聊SQL DB的死锁问题。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8