【C++多线程编程】(九)之 死锁(deadlock)及如何避免

2023-12-25 10:27:41

死锁(Deadlock)是多线程或多进程并发编程中一种常见的问题,它发生在两个或多个线程(或进程)互相等待对方释放资源,导致所有参与的线程都无法继续执行的状态。

典型的死锁场景涉及多个资源和多个线程,并且每个线程都在等待其他线程释放资源。死锁的产生通常包括以下四个必要条件,也称为死锁的四个必要条件:

  1. 互斥条件(Mutual Exclusion): 某个资源一次只能被一个线程(或进程)占用,其他线程必须等待释放。
  2. 持有和等待条件(Hold and Wait): 某个线程(或进程)持有至少一个资源,并在等待获取其他资源。
  3. 不可抢占条件(No Preemption): 资源不能被抢占,只能在持有资源的线程(或进程)释放后才能被其他线程获取。
  4. 循环等待条件(Circular Wait): 存在一个等待循环,即一组线程(或进程)互相等待,形成一个循环。

当这四个条件同时满足时,就有可能导致死锁的发生。死锁是并发编程中的一种非常棘手的问题,因为它不仅会导致程序无法继续执行,还很难被检测和解决。

避免死锁的一些常用策略包括:

  • 破坏互斥条件: 允许多个线程(或进程)共享资源。
  • 破坏持有和等待条件: 一次性获取所有需要的资源,或者在没有足够资源时释放已经持有的资源。
  • 破坏不可抢占条件: 允许抢占资源。
  • 破坏循环等待条件: 给资源编号,线程(或进程)按顺序请求资源,避免循环等待。

死锁是一个需要仔细设计和管理的问题,因此在编写多线程或多进程的程序时,需要谨慎地考虑资源的获取和释放顺序,以及采取适当的同步机制来避免死锁的发生。

这个程序演示了如何使用互斥量和独占锁来保护共享资源(num_things),以确保在多线程环境中对这些资源的访问是安全的。使用 std::unique_lockstd::defer_lock 参数来延迟锁定,以便稍后手动调用 std::lock 同时锁定两个互斥量,从而避免死锁的发生。

#include <mutex>
#include <thread>
#include <iostream>

// 定义一个包含数量信息和互斥量的结构体 Box
struct Box {
    explicit Box(int num) : num_things{num} {}
    int num_things;
    std::mutex m;
};

// 定义一个转账函数,将一定数量的东西从一个 Box 转移到另一个 Box
void transfer(Box &from, Box &to, int num)
{
    // 使用 std::unique_lock 来管理互斥量,但不立即上锁(defer_lock)
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);

    // 同时锁定两个互斥量,以避免死锁
    std::lock(lock1, lock2);  // 或者使用 lock1.lock() 和 lock2.lock()

    // 在临界区内进行操作,从一个 Box 中减去一定数量,同时将这些东西加到另一个 Box
    from.num_things -= num;
    to.num_things += num;

    // 作用域结束时,std::unique_lock 的析构函数会自动解锁互斥量
}

int main()
{
    // 创建两个 Box 对象,分别初始化数量
    Box acc1(100);
    Box acc2(50);

    // 创建两个线程,分别执行 transfer 函数进行转账操作
    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);

    // 等待两个线程执行结束
    t1.join();
    t2.join();

    // 输出两个 Box 的最终数量
    std::cout << "acc1 num_things: " << acc1.num_things << std::endl;
    std::cout << "acc2 num_things: " << acc2.num_things << std::endl;

    return 0;
}

std::ref 是 C++ 标准库中的一个函数模板,用于将一个对象包装成一个引用,从而能够在函数调用中传递引用语义而不是传值语义。,std::ref(acc1)acc1 这个对象包装成一个引用,使得在 std::thread 的构造函数中能够传递引用而不是拷贝对象。如果不使用 std::ref,则 std::thread 的构造函数默认会拷贝传递参数。

等价于

std::thread t1(transfer, &acc1, &acc2, 10);
std::thread t2(transfer, &acc2, &acc1, 5);

文章来源:https://blog.csdn.net/qq_54199287/article/details/135159208
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。