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();
    }
}
相关推荐
Lbs_gemini060328 分钟前
C++研发笔记14——C语言程序设计初阶学习笔记12
c语言·开发语言·c++·笔记·学习
我的老子姓彭3 小时前
C++学习笔记
c++·笔记·学习
hefaxiang3 小时前
【C++】数组
开发语言·c++
哎呦,帅小伙哦4 小时前
C++ 异步编程的利器std::future和std::promise
开发语言·c++
新兴AI民工4 小时前
C++中的操作系统级信号处理——signal与sigaction
c++·信号处理·signal·sigint·sigaction·操作系统信号处理
iiiiiiimp5 小时前
C++创建动态链接库(附原因说明)
开发语言·c++
立志成为master6 小时前
HBU算法设计第五章(回溯)
数据结构·c++·算法·dfs
miilue6 小时前
【C++】关于 Visual Studio 的使用技巧(保姆级教程)
c++·visual studio
橘子真甜~6 小时前
20. C++STL 6(详解list的使用,vector和list的比较和优缺点)
开发语言·数据结构·c++·list
三小尛7 小时前
日期类的实现
开发语言·c++