死锁(Deadlock)是多线程或多进程并发编程中一种常见的问题,它发生在两个或多个线程(或进程)互相等待对方释放资源,导致所有参与的线程都无法继续执行的状态。
典型的死锁场景涉及多个资源和多个线程,并且每个线程都在等待其他线程释放资源。死锁的产生通常包括以下四个必要条件,也称为死锁的四个必要条件:
当这四个条件同时满足时,就有可能导致死锁的发生。死锁是并发编程中的一种非常棘手的问题,因为它不仅会导致程序无法继续执行,还很难被检测和解决。
避免死锁的一些常用策略包括:
死锁是一个需要仔细设计和管理的问题,因此在编写多线程或多进程的程序时,需要谨慎地考虑资源的获取和释放顺序,以及采取适当的同步机制来避免死锁的发生。
这个程序演示了如何使用互斥量和独占锁来保护共享资源(num_things
),以确保在多线程环境中对这些资源的访问是安全的。使用 std::unique_lock
的 std::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);