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,因为它是标准库提供的安全机制,代码更简洁,不易出错。"

为什么需要避免死锁?

死锁是多线程编程中最常见的问题之一,它会导致程序完全停止响应。在面试中,理解死锁的原因和避免方法,能展示你对多线程编程的深入理解。

死锁的四个必要条件

  1. 互斥条件:资源不能被共享,只能由一个线程使用
  2. 占有并等待:线程持有至少一个资源,并等待获取其他被占用的资源
  3. 不可抢占:已获得的资源不能被其他线程强行抢占
  4. 循环等待:存在一个线程等待环,每个线程都在等待下一个线程持有的资源

通过给锁编号并按照顺序获取,我们破坏了"循环等待"条件,从而避免了死锁。

总结

方法 优点 缺点
std::lock 简洁、安全、标准库提供 需要C++11及以上
给锁编号 理解原理、可手动实现 代码稍复杂,需要手动处理顺序
超时获取锁 避免永久等待 可能需要额外的逻辑处理超时情况

在C++面试中,正确回答这个问题,特别是提到std::lock和给锁编号的策略,能展示你对多线程编程和死锁问题的深入理解。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐