C++场景题:避免两把锁同时获取的死锁问题
1. 同时拿到两把锁才能继续往下执行——怎么实现
在C++中,要同时获取两把锁并避免死锁,最推荐的方法是使用std::lock函数,它能安全地同时获取多个锁,避免死锁。
实现方法
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mutex1;
std::mutex mutex2;
void threadFunction() {
// 安全地同时获取两个锁(避免死锁)
std::lock(mutex1, mutex2);
// 两个锁都已获取,可以安全地执行临界区代码
std::cout << "Both locks acquired, executing critical section." << std::endl;
// 释放锁(按相反顺序释放)
std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
// 或者使用:
// std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
// std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
std::cout << "All threads completed." << std::endl;
return 0;
}
为什么这种方法能避免死锁?
std::lock函数内部实现了锁的顺序获取策略。它会按照一定的顺序(通常是按照锁的地址从小到大)获取锁,确保所有线程都按照相同的顺序获取锁,从而避免了死锁。
2. 给锁编号——避免死锁的另一种方法
如果不想使用std::lock,另一种常见方法是给锁编号并按照编号顺序获取锁。
实现方法
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mutex1;
std::mutex mutex2;
void threadFunction() {
// 按照锁的地址顺序获取锁(避免死锁)
if (&mutex1 < &mutex2) {
std::lock_guard<std::mutex> lock1(mutex1);
std::lock_guard<std::mutex> lock2(mutex2);
} else {
std::lock_guard<std::mutex> lock1(mutex2);
std::lock_guard<std::mutex> lock2(mutex1);
}
// 两个锁都已获取,可以安全地执行临界区代码
std::cout << "Both locks acquired in order, executing critical section." << std::endl;
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
std::cout << "All threads completed." << std::endl;
return 0;
}
为什么给锁编号能避免死锁?
这种方法基于统一的锁获取顺序原则:
- 所有线程都按照锁的地址顺序(或编号顺序)获取锁
- 例如,所有线程都先获取地址较小的锁,再获取地址较大的锁
- 这样可以确保不会出现"线程A持有锁1等待锁2,线程B持有锁2等待锁1"的死锁情况
与哲学家就餐问题的联系
这与知识库[2]中提到的哲学家就餐问题解决方案非常相似:
"让偶数编号的哲学家「先拿左边的叉子后拿右边的叉子」,奇数编号的哲学家「先拿右边的叉子后拿左边的叉子」。"
在哲学家就餐问题中,给叉子编号并按照编号顺序获取叉子,是避免死锁的标准方法。同样,在C++多线程编程中,给锁编号并按顺序获取,是避免死锁的常用策略。
面试金句
"在C++多线程编程中,要同时获取多把锁避免死锁,最优雅的解决方案是使用std::lock函数,它会自动按照安全的顺序获取多个锁。如果需要手动实现,我们可以通过给锁编号并按照编号顺序获取锁来避免死锁。这种方法基于一个简单但关键的原则:所有线程都按照相同的顺序获取锁。在哲学家就餐问题中,我们通过给叉子编号并规定获取顺序来避免死锁,C++中也是同样的原理。在实际开发中,我优先使用std::lock,因为它是标准库提供的安全机制,代码更简洁,不易出错。"
为什么需要避免死锁?
死锁是多线程编程中最常见的问题之一,它会导致程序完全停止响应。在面试中,理解死锁的原因和避免方法,能展示你对多线程编程的深入理解。
死锁的四个必要条件
- 互斥条件:资源不能被共享,只能由一个线程使用
- 占有并等待:线程持有至少一个资源,并等待获取其他被占用的资源
- 不可抢占:已获得的资源不能被其他线程强行抢占
- 循环等待:存在一个线程等待环,每个线程都在等待下一个线程持有的资源
通过给锁编号并按照顺序获取,我们破坏了"循环等待"条件,从而避免了死锁。
总结
| 方法 | 优点 | 缺点 |
|---|---|---|
std::lock |
简洁、安全、标准库提供 | 需要C++11及以上 |
| 给锁编号 | 理解原理、可手动实现 | 代码稍复杂,需要手动处理顺序 |
| 超时获取锁 | 避免永久等待 | 可能需要额外的逻辑处理超时情况 |
在C++面试中,正确回答这个问题,特别是提到std::lock和给锁编号的策略,能展示你对多线程编程和死锁问题的深入理解。
更多推荐
所有评论(0)