当前位置:嗨网首页>书籍在线阅读

17-死锁

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

7.6.2 死锁

对于线程而言,需求链带来痛苦,导致解决方案变得更加痛苦,这几乎是个残酷的讽刺。我们想要具有并发性的线程,而该并发性却带来了竞争条件。因此,我们引入了互斥,但互斥又带来了新的编程bug:死锁。

死锁是指两个线程都在等待另一个线程结束,因此两个线程都不能结束。在互斥场景下,两个线程都在等待对方持有的互斥对象。另一个场景是当某个线程被阻塞了,等待自己已经持有的互斥体。调试死锁往往很需要技巧,因为程序本身并没有崩溃。相反,它只是不再向前执行,因为越来越多的线程都在等待锁,而这一天却永远也不会来。

多线程在火星上的灾难 有很多真实的线程带来悲剧的故事,其中一个最让人印象深刻的是Mars Pathfinder,在1997年7月成功到达火星表面,结果却是它要分析火星气候和地理的使命不断地被系统重新设置干扰。 Mars PathFinder的引擎是实时的,高度多线程化的嵌入式内核(不是Linux)。内核提供对线程的抢占式调度。类似Linux,实时线程包含优先级,给定优先级的线程总是在低优先级线程执行之前运行。引发这个bug的主要是三大线程:一个低优先级线程收集气象学数据,一个中间优先级的线程和地球通信,还有一个高优先级的线程管理存储。正如在本章前面所看到的(参见7.5节),同步对于阻止数据竞争是至关重要的,因此线程通过互斥管理并发。需要注意的是,互斥对低优先级的气象线程(生成数据)和高优先级的存储线程(管理该数据)进行同步。 气象线程不会频繁运行,负责对太空船的各个传感器进行轮询。线程会获取互斥,把气象数据写到存储子系统中,最后释放互斥。存储线程运行更频繁,对系统事件进行响应。在管理存储子系统之前,该线程还会获取互斥体。如果没有获取到互斥体,它会休眠,直到气象线程释放它。 从目前看,一切都正常。但是,在某些情况下,当气象线程持有互斥而存储线程等待互斥时,通信线程会醒来并运行。由于通信线程比气象线程优先级更高,前者会先抢先运行。不幸的是,通信线程是个运行时间很长的任务:火星离我们实在太远啦!因此,在整个通信线程操作期间,气象线程都没有运行。这看起来是由于设计问题,因为是按优先级来的。但是气象线程持有资源(互斥),而存储线程需要该资源。因此,一个优先级较低的线程(通信)间接“抢占”了更高级优先级线程(存储)。最终,系统发现存储线程没有继续向前执行,认为出现故障,因此执行系统重置操作。这个例子就是经典的“优先级倒置(priority inversion)”问题。 解决该问题的技术称为“优先级继承(priority inheritance)”,持有资源的进程继承等待该资源的最高优先级进程的优先级。在这个示例中,优先级低的气象线程在持有互斥期间,可以继承优先级高的存储线程。通过这种方式,通信线程就可以抢占气象线程,支持快速释放互斥,并调度存储线程。如果你觉得这个方案还是不保险,那就使用单线程编程吧!

避免死锁

避免死锁很重要,要想持续、安全地做到这一点,唯有从一开始的设计中就为多线程程序设计好锁的机制。互斥体应该和数据关联,而不是和代码关联,从而有清晰的数据层(因而互斥也清晰了)。举个例子,一种简单的死锁方式是“ABBA死锁”,也称为“死锁拥抱”。当一个线程先获取互斥锁A,然后获取互斥锁B,而另一个线程先获取互斥锁B,然后是A(即ABBA),就会发生这种情况。在正确的情况下,这两个线程都可以成功获取第一个互斥锁:线程1持有A,线程2持有B。当它们要获取另一个互斥时,发现被另一个线程持有,因此这两个线程都阻塞在那里。因为每个持有互斥的线程也在等待另一个互斥,双方都没有释放自己持有的互斥,因而导致线程死锁。

解决这个问题需要有明确的原则:必须总是先获取互斥A,然后获取互斥B。由于程序的复杂性和同步机制变得更加复杂,越到后来加强这些原则只会变得更加困难。早点开始,设计简洁。