MST

星途 面试题库

面试题:C++多线程全局变量引用下的死锁预防与优化

假设在一个C++项目中,存在多个全局变量,如 `Mutex mutex1`, `Mutex mutex2`,以及全局数据结构 `std::map<int, int> globalMap;`。有多个线程,其中一些线程需要按顺序获取 `mutex1` 和 `mutex2` 锁来操作 `globalMap`,而另一些线程按相反顺序获取锁操作 `globalMap`。阐述如何检测和预防这种情况下可能出现的死锁问题,并讨论如何对整个多线程访问全局变量的架构进行优化,以提高系统的并发性能和稳定性。
38.7万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

检测死锁

  1. 死锁检测算法
    • 资源分配图算法:可以构建资源分配图(RAG),其中节点表示线程和资源(这里的锁和 globalMap 等数据结构可视为资源),边表示线程对资源的请求和分配关系。通过对RAG进行分析,如使用深度优先搜索(DFS)算法来检测图中是否存在环,若存在环则可能发生死锁。
    • 超时检测:为每个锁的获取操作设置一个超时时间。例如,使用 std::unique_lock 的带超时版本 std::unique_lock<std::mutex> lock(mutex1, std::chrono::seconds(2));,如果在规定时间内没有获取到锁,则认为可能出现死锁情况,线程可以进行相应处理,如释放已获取的锁并重新尝试。
  2. 日志记录和分析
    • 在锁获取和释放操作的关键位置添加日志记录,例如使用 std::cout 或者专业的日志库(如 spdlog)记录线程ID、锁获取和释放的时间、锁的类型等信息。通过分析日志文件,可以发现异常的锁获取顺序和长时间未释放锁的情况,从而定位死锁问题。

预防死锁

  1. 统一锁获取顺序
    • 所有线程都按照相同的顺序获取锁,例如先获取 mutex1,再获取 mutex2。这样可以避免形成死锁的循环等待条件。例如:
// 所有线程都按此顺序获取锁
std::unique_lock<std::mutex> lock1(mutex1);
std::unique_lock<std::mutex> lock2(mutex2);
// 操作 globalMap
globalMap[1] = 1;
  1. 使用锁层次结构
    • 为锁分配层次编号,例如 mutex1 的层次编号为1,mutex2 的层次编号为2。线程在获取锁时,只能从低层次编号的锁开始获取,然后按顺序获取高层次编号的锁。在获取锁之前,线程需要检查当前已获取的锁的层次编号,如果新锁的层次编号低于当前已获取锁的最高层次编号,则不能获取该锁,从而防止死锁。
  2. 使用 std::lock
    • 使用 std::lock 函数来同时获取多个锁,它会以一种无死锁的方式获取所有锁。例如:
std::lock(mutex1, mutex2);
std::unique_lock<std::mutex> lock1(mutex1, std::adopt_lock);
std::unique_lock<std::mutex> lock2(mutex2, std::adopt_lock);
// 操作 globalMap

架构优化

  1. 细粒度锁
    • 如果 globalMap 的操作可以细分为多个独立部分,可以为 globalMap 的不同部分设置不同的锁。例如,如果 globalMap 中某些键值对是经常独立操作的,可以为这些部分分别设置锁,减少锁的粒度,提高并发性能。例如:
std::map<int, int> globalMap1;
std::map<int, int> globalMap2;
Mutex mutexMap1;
Mutex mutexMap2;
// 线程1操作 globalMap1
{
    std::unique_lock<std::mutex> lock(mutexMap1);
    globalMap1[1] = 1;
}
// 线程2操作 globalMap2
{
    std::unique_lock<std::mutex> lock(mutexMap2);
    globalMap2[2] = 2;
}
  1. 读写锁
    • 如果对 globalMap 的操作读多写少,可以使用读写锁(如 std::shared_mutex)。读操作时,多个线程可以同时获取读锁,提高并发读性能;写操作时,获取写锁,独占访问 globalMap,保证数据一致性。例如:
std::shared_mutex globalMapMutex;
// 读操作
{
    std::shared_lock<std::shared_mutex> lock(globalMapMutex);
    int value = globalMap[1];
}
// 写操作
{
    std::unique_lock<std::shared_mutex> lock(globalMapMutex);
    globalMap[1] = 1;
}
  1. 无锁数据结构
    • 对于 globalMap,如果适用,可以考虑使用无锁数据结构,如 std::unordered_map 的无锁版本(一些第三方库提供)。无锁数据结构通过使用原子操作等方式避免了锁的争用,提高并发性能,但实现和使用较为复杂,需要仔细考虑数据一致性和内存模型等问题。
  2. 线程池
    • 使用线程池来管理线程的创建和销毁,可以减少线程创建和销毁的开销。例如,使用 std::thread 结合 std::queue 实现简单线程池,或者使用专业的线程池库(如 boost::thread_pool)。线程池可以根据系统资源和任务负载动态调整线程数量,提高系统的整体性能和稳定性。