操作系统导论习题解答(32. Concurrency Bugs)
Common Concurrency Problems
带着问题:如何处理常见的并发错误问题?
1. What Types Of Bugs Exist?
主要研究4个开源应用:
- MYSQL
- Apache
- Mozilla
- OpenOffice
2. Non-Deadlock Bugs
大部分并发问题都是non-deadlock bugs。此类问题主要分为两类:
- atomicity violation bugs
- order violation bugs
2.1 Atomicity-Violation Bugs
下图是MYSQL的一个并发问题:
上述代码会产生问题:线程1检查了thd->proc_info
为非NULL值,准备进入fputs
时被打断;然后线程2开始运行,把本来不是NULL值的thd->proc_info
变成了NULL值;最后线程1再继续运行时发生崩溃。
解决这类问题可以使用锁(lock)。
2.2 Order-Violation Bugs
下图是另一方面并发问题:
上述代码问题:线程2运行时它觉得mThread已经存在了,如果mThread为NULL或未初始化,那么线程2就会崩溃。
解决这类问题可以使用条件变量(condition variables)和锁(lock)。
2.3 Non-Deadlock Bugs: Summary
不是所有的non-deadlock bugs都能很好的如上述方法处理,一部分Bugs需要我们对其有很深的理解。
3. Deadlock Bugs
一个双方都等待对方释放锁的例子:
上述代码不是一定发生死锁(deadlock),而只是有可能:线程1获取锁L1后发生线程转换,线程2获取锁L2,这样才会产生deadlock。
3.1 Why Do Deadlocks Occur?
产生死锁的原因:
- 在大型代码库中,组件之间会产生复杂的依赖关系(complex dependencies)。
- 封装(encapsulation)的性质。
3.2 Conditions for Deadlock
发生死锁需要满足四个条件:
- Mutual exclusion
- Hold-and-wait
- No preemption
- Circular wait
上述任一条件不满足都不会发生死锁,所以解决死锁可以从4四个方面入手。
3.3 Prevention
3.3.1 Circular Wait
解决circular wait最直接的方法就是在获取锁的时候提供total ordering(总顺序)。当然,对于小型系统而言,能够简单实现;但是对于大型系统,锁非常多,提供总顺序不太可能,所以,一个有效的方法就是提供部分顺序(partial ordering)。
3.3.2 Hold-and wait
解决该问题的方法如下:一次获取所有锁。
3.3.3 No Preemption
很多系统都提供很灵活的接口处理这个问题。一个例子如下:
上述方法解决死锁但是又产生了一个新的问题活锁(livelock):两个线程都可以反复尝试此代码,而又多次未能获取两个锁。
解决livelock的方法就是使用trylock:线程获取锁L1后,获取锁L2之前必须释放获取锁L1时分配的内存。
3.3.4 Mutual Exclusion
解决此类问题方法:使用功能强大的硬件指令,构造不需要显式锁的数据结构。
看一个例子,假设要调用硬件提供的指令compare-and-swap:
要做加值操作:
可以看到关键部分没有加锁,而是使用硬件提供的指令。
再看一个链表插入例子:
上述代码执行简单插入,但是在多线程中会造成race conditon。
对其进行修改:
上述代码对关键部分加锁。我们接下来不使用锁而使用硬件指令:
上述代码在多线程中有个问题:如果其他线程同时成功换入了新的头节点,则此操作失败,从而导致该线程使用新的头节点再次重试。
4. Deadlock Avoidance via Scheduling
在某些场景下避免死锁发生比阻止死锁发生更好。
假设有两个锁,四个线程。每个线程获取锁的状态如下:
只要T1、T2不同时运行,就不会发生死锁。调度程序可以如下:
注:T3和T1、T3和T2可以交替执行。
再看另一种情况:
由于T1、T2、T3都获取锁L1、L2,故只要T1、T2、T3不同时运行,就不会发生死锁。
5. Detect and Recover
最后一种解决死锁的策略:允许死锁发生,但是发生后对其进行相应的处理。
最简单的例子:操作系统运行了一个月,你只需重启就行了。