非死锁缺陷
违反原子性 (Atomicity Violation)
违反原子性是指违反了多次内存访问中预期的可串行性,即代码本意是原子的,但在执行中未强制实现。
例如,在以下代码中,线程 1 检查非空后若被中断,线程 2 将其置空,线程 1 恢复执行时就会引用空指针导致程序奔溃:
// Thread 1
if (thd->proc_info) {
fputs(thd->proc_info, ...);
}
// Thread 2
thd->proc_info = NULL;修复这类问题的方法通常是给共享变量的访问加锁,以确保操作的原子性:
pthread_mutex_lock(&proc_info_lock);
if (thd->proc_info) {
fputs(thd->proc_info, ...);
}
pthread_mutex_unlock(&proc_info_lock);违反顺序 (Order Violation)
违反顺序是指两个内存访问的预期顺序被打破,即 A 应在 B 前执行,但实际运行中未保证。
例如,在以下代码中,线程 2 假定 mThread 已初始化。若线程 1 未先执行,线程 2 会因引用空指针奔溃:
// Thread 1
void init() {
mThread = PR_CreateThread(mMain, ...);
}
// Thread 2
void mMain(...) {
mState = mThread->State;
}修复这类问题通常使用条件变量来强制执行顺序:
// Thread 1
pthread_mutex_lock(&mtLock);
mtInit = 1;
pthread_cond_signal(&mtCond);
pthread_mutex_unlock(&mtLock);
// Thread 2
pthread_mutex_lock(&mtLock);
while (mtInit == 0)
pthread_cond_wait(&mtCond, &mtLock);
pthread_mutex_unlock(&mtLock);
mState = mThread->State;死锁缺陷
死锁是指两个或多个线程互相等待对方释放资源,导致程序无法继续执行。
产生原因
死锁的产生通常源于大型代码库中组件之间复杂的循环依赖(如操作系统中虚拟内存与文件系统的交互)。此外,软件模块化带来的封装隐藏了实现细节,也容易导致看似无关的接口调用产生死锁(如 Java 的 Vector.AddAll() 内部获取多个锁时的顺序问题)。
产生条件
死锁的产生必须同时满足以下四个条件:
- 互斥:线程对需要的资源进行互斥访问。
- 持有并等待:线程持有资源的同时,又在等待其他资源。
- 非抢占:线程获得的资源不能被强行剥夺。
- 循环等待:线程之间存在环路,环路上每个线程都持有一个资源,而该资源又是下一个线程要申请的。
预防死锁
破坏上述四个条件中的任意一个即可预防死锁。
破坏循环等待
可以为系统中的锁提供全序或偏序。一个实用的技巧是通过锁的地址来强制加锁顺序(如总是先抢低地址锁再抢高地址锁),从而确保无论传入参数顺序如何,加锁顺序始终固定。
破坏持有并等待
可以通过原子地抢锁来实现。例如先抢到一个全局的 prevention 锁,然后再抢其他所需的所有锁。但这种做法破坏了封装,且因为要提前抢到所有锁而降低了并发度。
破坏非抢占
可以使用 trylock() 尝试获得锁,若失败(返回 -1)则释放已持有的锁并稍后重试。这可能导致两个线程不断重复尝试和失败的活锁 (Livelock) 问题,通常可以通过在重试前随机等待一段时间来解决。
破坏互斥
可以完全避免使用锁,转而利用强大的硬件指令(如比较并交换 Compare-And-Swap)构造无等待 (Wait-Free) 数据结构。例如以下无锁原子递增的实现:
void AtomicIncrement(int *value, int amount) {
do {
int old = *value;
} while (CompareAndSwap(value, old, old + amount) == 0);
}通过调度避免死锁
如果在调度时了解全局信息(即不同线程运行中对锁的需求),就可以避免可能产生死锁的线程组合同时运行。但这种方法适用场景局限(如仅适用于需要提前知道所有任务及所需锁的嵌入式系统),且会限制并发,因此不是广泛使用的通用方案。
检查和恢复
最后一种策略是允许死锁偶尔发生,在检查到死锁时再采取行动(如重启系统或人工干预)。许多数据库系统就使用死锁检测器定期构建资源图来检查循环。正如 TOM WEST 定律所言:“不是所有值得做的事情都值得做好”,如果死锁很少发生且影响很小,就不应花费大量精力去预防。