C++ 并发专题 - 使用 std::lock 避免多线程死锁

一:std::lock 介绍

std::lock 是 C++11 引入的一个函数,它可以原子性地锁定多个互斥量(mutex),确保在多个线程竞争锁时避免死锁。它的主要用途是在多个互斥量之间避免因不同线程的不同锁定顺序而导致的死锁问题。

二: std::lock 例子

下面是一个例子,演示了如何在多个线程之间原子性的锁定多个互斥量,避免死锁的情况发生:

复制代码
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>

// 定义一个结构体,包含一个互斥量,用于保护共享资源
struct CriticalData
{
    std::mutex mut; // 互斥量,用于保护资源
};

// 模拟死锁的函数
void deadLock(CriticalData& a, CriticalData& b)
{
    // 锁定第一个互斥量
    std::unique_lock<std::mutex> guard1(a.mut);
    std::cout << "Thread: " << std::this_thread::get_id() << " locking the first mutex" << '\n';

    // 睡眠1毫秒,模拟一些操作
    std::this_thread::sleep_for(std::chrono::milliseconds(1));

    // 锁定第二个互斥量,这里会出现死锁问题,因为另外一个线程可能已经锁定了第二个互斥量
    std::unique_lock<std::mutex> guard2(b.mut);
    std::cout << "Thread: " << std::this_thread::get_id() << " locking the second mutex" << '\n';
}

// 使用 std::lock 避免死锁的函数
void NoDeadLock(CriticalData& a, CriticalData& b)
{
    // 使用 std::lock 原子性地锁定两个互斥量
    std::lock(a.mut, b.mut);  // 确保以相同的顺序锁定两个互斥量,避免死锁
    std::cout << "Thread: " << std::this_thread::get_id() << " locking them both atomically" << '\n';
    
    // 使用 std::lock_guard 管理锁的生命周期,std::adopt_lock 表示锁已经被 std::lock 锁定
    std::lock_guard<std::mutex> lock1(a.mut, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(b.mut, std::adopt_lock);
}


int main() {

    std::cout << '\n';

    // 创建两个共享数据,分别包含互斥量
    CriticalData c1;
    CriticalData c2;

    // 模拟死锁的线程:互斥量锁定顺序可能导致死锁
    //std::thread t1([&] {deadLock(c1, c2); });
    //std::thread t2([&] {deadLock(c2, c1); });
    
    // 使用 std::lock 避免死锁的线程:保证两个互斥量原子性锁定
    std::thread t1([&] {NoDeadLock(c1, c2); });
    std::thread t2([&] {NoDeadLock(c2, c1); });

    // 等待线程执行完成
    t1.join();
    t2.join();

    // 如果输出这句话,表示没有发生死锁
    std::cout << "If you print this sentence, it means there is no deadlock\n" << std::endl;

    std::cout << '\n';

}

三:std::lock 的工作原理

std::lock 是一个基于"分布式锁"的算法,它通过将所有传入的互斥锁排序并使用"试探性"锁定的方法来避免死锁。其原理通常是:

  1. 获取锁的顺序:线程在请求锁时会对所有锁进行排序(通常是按互斥量的内存地址排序)。这样就能确保所有线程按照相同的顺序尝试锁定互斥量。
  2. 试探性锁定:线程首先尝试获取所有互斥锁。如果某个锁无法立即获得,所有其他锁会被释放,线程会等待一段时间后重新尝试。
  3. 原子性锁定:一旦所有锁都成功获得,线程将继续执行。通过原子性地锁定所有传入的互斥量,std::lock 可以避免死锁。

下面是linux下的一种可能实现:

cpp 复制代码
template <typename Mutex1, typename Mutex2, typename... Mutexes>
void lock(Mutex1& m1, Mutex2& m2, Mutexes&... m) {
    // 创建一个锁的数组,所有传入的互斥量都会被传递给 lock 函数
    // 对互斥量排序
    std::array<Mutex*, sizeof...(m) + 2> mutexes = {&m1, &m2, &m...};

    // 排序互斥量
    std::sort(mutexes.begin(), mutexes.end());

    // 尝试锁定所有互斥量
    while (true) {
        // 尝试获取每个锁
        bool success = true;
        for (auto& m : mutexes) {
            if (pthread_mutex_trylock(m) != 0) {
                success = false;
                break;
            }
        }

        if (success) {
            return; // 如果成功锁定所有互斥量,退出函数
        }

        // 如果锁定失败,释放已锁定的互斥量并重试
        for (auto& m : mutexes) {
            pthread_mutex_unlock(m);
        }
        // 等待一段时间,防止忙等待
        std::this_thread::yield();
    }
}
相关推荐
R-G-B8 小时前
【15】OpenCV C++实战篇——fitEllipse椭圆拟合、 Ellipse()画椭圆
c++·人工智能·opencv·fitellipse椭圆拟合·ellipse画椭圆·椭圆拟合·绘制椭圆
界面开发小八哥10 小时前
MFC扩展库BCGControlBar Pro v36.2:MSAA和CodedUI测试升级
c++·mfc·bcg·界面控件
极客BIM工作室13 小时前
C++ 限制类对象数量的技巧与实践
开发语言·javascript·c++
郝学胜-神的一滴13 小时前
Horse3D引擎研发笔记(四):在QtOpenGL下仿three.js,封装EBO绘制四边形
c++·3d·unity·游戏引擎·godot·图形渲染·虚幻
终焉代码14 小时前
【C++】STL二叉搜索树——map与set容器的基础结构
开发语言·数据结构·c++
源代码•宸15 小时前
深入浅出设计模式——行为型模式之观察者模式 Observer
开发语言·c++·经验分享·观察者模式·设计模式·raii
小马敲马15 小时前
[4.2-2] NCCL新版本的register如何实现的?
开发语言·c++·人工智能·算法·性能优化·nccl
soilovedogs16 小时前
百度之星2024初赛第二场 BD202411染色
c++·算法·百度之星
啊阿狸不会拉杆17 小时前
《算法导论》第 15 章 - 动态规划
数据结构·c++·算法·排序算法·动态规划·代理模式
2401_8582861118 小时前
CD64.【C++ Dev】多态(3): 反汇编剖析单继承下的虚函数表
开发语言·c++·算法·继承·面向对象·虚函数·反汇编