(六)解决死锁问题
死锁(Deadlock)是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(或线程)称为死锁进程(或线程)。
死锁的发生通常需要同时满足四个必要条件:
- 互斥条件:某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程(或线程)占有。
- 占有且等待条件:进程(或线程)至少已经占有一个资源,但又申请新的资源。
- 不可抢占条件:一个进程(或线程)所占有的资源在其使用完之前,不能被其他进程(或线程)强行夺走,只能由该进程(或线程)用完之后主动释放。
- 循环等待条件:存在一个进程(或线程)等待序列{P1, P2, …, Pn},其中P1等待P2所占有的某个资源,P2等待P3所占有的某个资源,…,而Pn等待P1所占有的某个资源,从而形成一个进程(或线程)循环等待的局面。
例如,一条生产线上,每一个工具同一时刻只能被一人使用(互斥条件),每个人手里已经有一个工具了,还想要下一个人手里的工具(占有且等待),每个人的工具都不会被被动地剥夺(不可抢占条件),每个人都在等着下一个人手里的工具,而最后一个人又在等待第一个人手里的工具(循环等待条件)。
下面是一个有死锁问题的代码示例:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtxA;
std::mutex mtxB;
void threadFunction1() {
// 线程1先锁定互斥锁A
std::lock_guard<std::mutex> lockA(mtxA);
std::cout << "Thread 1: Locked mtxA, trying to lock mtxB\n";
// 试图锁定互斥锁B,若此时如果线程2已经锁定了mtxB,则线程1会阻塞
std::lock_guard<std::mutex> lockB(mtxB);
std::cout << "Thread 1: Locked mtxB\n";
}
void threadFunction2() {
// 线程2先锁定互斥锁B
std::lock_guard<std::mutex> lockB(mtxB);
std::cout << "Thread 2: Locked mtxB, trying to lock mtxA\n";
// 试图锁定互斥锁A,但此时如果线程1已经锁定了mtxA,则线程2会阻塞
std::lock_guard<std::mutex> lockA(mtxA);
std::cout << "Thread 2: Locked mtxA\n";
}
int main() {
// 创建两个线程
std::thread t1(threadFunction1);
std::thread t2(threadFunction2);
// 等待两个线程完成
t1.join();
t2.join();
return 0;
}
Thread 1: Locked mtxA, trying to lock mtxB
Thread 2: Locked mtxB, trying to lock mtxA
在这个示例中:
- 互斥条件:由
std::mutex
类保证,每个互斥锁在同一时间只能被一个线程锁定。 - 占有且等待条件:线程1锁定了
mtxA
并等待mtxB
,而线程2锁定了mtxB
并等待mtxA
。 - 不可抢占条件:一旦线程锁定了互斥锁,没有其他线程可以抢占它,直到原始线程调用解锁。
- 循环等待条件:线程1等待线程2释放
mtxB
,而线程2等待线程1释放mtxA
,形成了一个循环等待。
由于这四个条件都满足,因此这个程序可能会导致死锁。
为了解决上述代码中的死锁问题,我们可以使用一种称为“锁顺序”或“锁排序”的策略,即确保所有线程都以相同的顺序请求锁。这样可以避免循环等待的情况,从而防止死锁:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtxA;
std::mutex mtxB;
void threadFunction1() {
std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock); // 延迟锁定 mtxA
std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock); // 延迟锁定 mtxB
// 使用 std::lock 确保两个锁以正确的顺序被锁定
std::lock(lockA, lockB);
// mtxA 和 mtxB 都被锁定了,并且是按照正确的顺序
std::cout << "Thread 1: Both mtxA and mtxB are locked.\n";
}
void threadFunction2() {
std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock); // 延迟锁定 mtxB
std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock); // 延迟锁定 mtxA
// 使用 std::lock 确保两个锁以正确的顺序被锁定
std::lock(lockA, lockB);
// mtxA 和 mtxB 都被锁定了,并且是按照正确的顺序
std::cout << "Thread 2: Both mtxA and mtxB are locked.\n";
}
int main() {
// 创建两个线程
std::thread t1(threadFunction1);
std::thread t2(threadFunction2);
// 等待两个线程完成
t1.join();
t2.join();
return 0;
}
Thread 1: Both mtxA and mtxB are locked.
Thread 2: Both mtxA and mtxB are locked.
在上面的代码中使用 std::unique_lock
和 std::lock
来确保 mtxA
和 mtxB
以正确的顺序被锁定。std::lock
函数会尝试以原子方式锁定多个互斥锁,如果无法立即获得所有锁,它会阻塞直到所有锁都变得可用,或者抛出异常(取决于 std::lock
的配置)。
注意,std::unique_lock
的构造函数中使用了 std::defer_lock
参数来延迟锁定(std::lock_guard
不支持延迟锁定),这样我们才可以在调用 std::lock
时以原子方式锁定多个互斥锁。如果直接使用 std::lock_guard
而不延迟锁定,那么每个 std::lock_guard
会立即尝试锁定其对应的互斥锁,这样就无法使用 std::lock
来协调多个锁的锁定顺序了。