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();
    }
}
相关推荐
2401_890603407 小时前
C++ 继承
c++
果果燕8 小时前
C++ 学习笔记(二):类与对象—— 构造函数与析构函数
开发语言·c++
老洋葱Mr_Onion8 小时前
【C++】信息学奥赛CSP通关之路——CSP-J/S第一轮原创全真模拟试卷集(2026)卷四全卷解析
开发语言·c++
布莱克6059 小时前
理解 C/C++ 中的 void* 指针
c语言·c++
小小龙学IT11 小时前
simdjson:利用 SIMD 指令实现 GB/s 级 JSON 解析的 C++ 开源库
开发语言·c++·json
不正经学生11 小时前
C语言字符串函数进阶:安全版函数 + 错误处理 + 子串查找
c语言·开发语言·c++·算法·面试
三克的油11 小时前
算法基础-1
数据结构·c++·算法
鸿芯微控科技12 小时前
MFC量程怎么选?最小流量、满量程、调节比与误差计算
c++·python·mfc·流量控制·设备选型·质量流量控制器
依然鸣12 小时前
PTA团体程序设计天梯赛L2真题讲解L2-005-008
开发语言·数据结构·c++·算法·深度优先·pat考试·图论
欧特克_Glodon14 小时前
OpenCV计算机视觉开发入门与实践<七>:OpenCV 界面编程之窗口
c++·人工智能·opencv·计算机视觉