MST

星途 面试题库

面试题:C++多线程中的死锁问题及解决方案

在一个复杂的C++多线程程序中,可能会出现死锁情况。请详细分析死锁产生的四个必要条件,并给出至少两种避免死锁的策略以及如何在代码层面实现这些策略。同时,假设程序已经出现死锁,描述你会采取哪些步骤来定位和解决死锁问题。
41.9万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

死锁产生的四个必要条件

  1. 互斥条件:资源一次只能被一个线程使用,即某个资源在一段时间内只能由一个线程占有。例如,一个文件在同一时刻只能被一个线程打开进行写操作。
  2. 占有并等待条件:一个线程已经持有了至少一个资源,但又请求新的资源,并且在等待新资源的同时,不会释放已持有的资源。比如线程 A 持有资源 R1,又请求资源 R2,在等待 R2 时不释放 R1。
  3. 不可剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能由该线程自己释放。例如线程获取的锁,其他线程不能强行抢走。
  4. 循环等待条件:存在一个线程 - 资源的循环链,链中每个线程都在等待下一个线程所持有的资源。例如线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,线程 C 又等待线程 A 持有的资源。

避免死锁的策略及代码实现

  1. 破坏占有并等待条件
    • 策略:在申请新的资源前,先释放已持有的所有资源,然后一次性申请所需的全部资源。
    • 代码实现
#include <mutex>
#include <thread>
#include <iostream>

std::mutex m1, m2;

void threadFunction() {
    // 先释放所有已持有的锁
    std::unique_lock<std::mutex> lock1(m1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(m2, std::defer_lock);
    // 一次性申请所有需要的锁
    std::lock(lock1, lock2);
    std::cout << "Thread is working with both resources" << std::endl;
    // 工作完成后释放锁
    lock1.unlock();
    lock2.unlock();
}

int main() {
    std::thread t(threadFunction);
    t.join();
    return 0;
}
  1. 破坏循环等待条件
    • 策略:对资源进行排序,线程按照固定的顺序获取资源。
    • 代码实现
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <algorithm>

std::mutex m1, m2;

void threadFunction() {
    std::vector<std::mutex*> resources = {&m1, &m2};
    std::sort(resources.begin(), resources.end(), [](std::mutex* a, std::mutex* b) {
        return a < b;
    });
    for (auto mutexPtr : resources) {
        std::unique_lock<std::mutex> lock(*mutexPtr);
    }
    std::cout << "Thread is working with both resources" << std::endl;
    // 锁会在离开作用域时自动释放
}

int main() {
    std::thread t(threadFunction);
    t.join();
    return 0;
}

定位和解决死锁问题的步骤

  1. 收集信息
    • 日志记录:检查程序运行过程中的日志,查看是否有异常情况记录,例如线程进入等待状态但未按预期执行下一步操作等信息。
    • 系统监控:使用系统工具(如 Linux 下的 topps 等命令)查看进程状态,确定程序是否处于死锁状态,以及死锁进程占用的资源情况。
  2. 使用调试工具
    • GDB(GNU Debugger):如果程序运行在支持 GDB 的环境中,使用 GDB 附加到死锁进程。通过 info threads 命令查看所有线程的状态,找到处于阻塞状态的线程。然后使用 thread <thread_id> 切换到具体线程,通过 bt(backtrace)命令查看线程的调用栈,分析线程在等待什么资源。
    • Valgrind:Valgrind 工具集的 helgrind 工具可以检测多线程程序中的竞争条件和死锁。运行程序时使用 helgrind,它会报告可能导致死锁的资源竞争点。
  3. 分析代码
    • 检查资源获取顺序:查看代码中资源获取的顺序,确认是否存在循环等待的可能。如果发现资源获取顺序混乱,按照破坏循环等待条件的策略进行修改。
    • 检查锁的使用:检查锁的获取和释放逻辑,确保没有线程在持有锁的情况下无限期等待其他资源,遵循破坏占有并等待条件的原则优化代码。
  4. 修复和验证
    • 修复代码:根据分析结果对代码进行修改,如调整资源获取顺序、优化锁的使用逻辑等。
    • 重新测试:修改代码后,进行全面的测试,包括多线程并发测试,确保死锁问题得到解决,并且没有引入新的问题。