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();
    }
}
相关推荐
水饺编程8 分钟前
Visual Studio 软件操作:添加附加依赖项
c语言·c++·windows·visual studio
-To be number.wan28 分钟前
C++ 进阶技巧:如何让 cout << 自定义对象 正常输出?
开发语言·c++
序属秋秋秋1 小时前
《Linux系统编程之进程控制》【进程创建 + 进程终止】
linux·c语言·c++·操作系统·进程·进程创建·进程终止
上天_去_做颗惺星 EVE_BLUE1 小时前
C++学习:学生成绩管理系统
c语言·开发语言·数据结构·c++·学习
John_ToDebug1 小时前
Chromium WebUI 定制实践:从 C++ 注入到 JS 安全展示全链路解析
javascript·c++·chrome
水饺编程2 小时前
开源项目介绍:VirtuaNES 模拟器
c语言·c++·windows·visual studio
十五年专注C++开发2 小时前
CMake进阶:find_package使用总结
开发语言·c++·cmake·跨平台编译
闻缺陷则喜何志丹2 小时前
【计算几何】平面凸包
c++·数学·扫描线·凸包·单调性·上凸包·下凸包
-To be number.wan2 小时前
C++ 运算符重载入门:让“+”也能为自定义类型服务!
开发语言·c++
汉克老师2 小时前
GESP2025年12月认证C++一级真题与解析(编程题1 (小杨的爱心快递))
c++·数据类型·选择结构·格式输出