std::lock
是一个 C++ 并发工具,用于一次性锁住多个互斥锁(std::mutex),它通过确保锁定顺序一致,避免了死锁的发生。死锁是指程序中有多个共享资源,通常情况下,多个共享资源需要多个互斥锁来保护,以确保线程在访问这些资源时不会发生冲突。如果两个或更多线程在相互等待对方释放锁,就会导致它们无法继续执行,最终造成程序停滞或卡死。
例如,假设我们有两个共享资源 A 和 B,分别对应两个互斥锁 MA 和 MB。现在有两个线程 Thread1 和 Thread2,它们分别尝试访问资源 A 和 B。假设:
- Thread1 首先锁定 MA(锁定资源 A),然后尝试锁定 MB(锁定资源 B)。
- Thread2 首先锁定 MB(锁定资源 B),然后尝试锁定 MA(锁定资源 A)。
如果 Thread1 和 Thread2 同时执行,Thread1 会锁定 MA 并等待 MB,而 Thread2 会锁定 MB 并等待 MA。这时,两个线程互相等待对方释放锁,形成死锁,导致程序停滞。
1. 死锁场景
我们构造一个 银行账户转账操作 的多线程案例。在案例中,存在两个账户 A 和 B,我们需要做的是:
- 开启一个线程,来完成 A 账户向 B 账户的转账 200 元的操作
- 开启一个线程,来完成 B 账户向 A 账户的转账 100 元的操作
这个案例需要注意的是:A 向 B 转账时,我们需要同时锁定 A 账户和 B 账户,避免同时其他的账户向这两个账户转账。
我们目前的设计思路有两个:
- 创建一个全局互斥锁。当 A 和 B 转账时,先获得这把锁,再进行转账操作。这种方法虽然能确保数据的安全性,但缺点是限制了并发性。如果 C 和 D 账户也在进行转账操作,它们也必须等待获得锁,从而影响整体的并发性能。
- 给每一个账户创建一个互斥锁。当 A 和 B 转账时,我们直接获得 A 和 B 的锁,再进行转账。对于 C 和 D 的转账操作不会受到影响,有利于提高系统的并发性。
通过分析,我们认为第二种实现思路更好。下面的是实现代码:
#if 1 #include <iostream> #include <thread> #include <mutex> #include <chrono> struct BankAccount { BankAccount(size_t account_id, std::string name, double balance) : account_id(account_id), name(name), balance(balance) { } void printBalance() { std::cout << name << "账户余额为:" << balance << std::endl; } size_t account_id; std::string name; double balance; std::mutex mtx; }; void bank_transfer(BankAccount& from, BankAccount& to, double amount) { std::this_thread::sleep_for(std::chrono::seconds(1)); from.mtx.lock(); to.mtx.lock(); if (from.balance >= amount) { from.balance -= amount; to.balance += amount; std::cout << "从" << from.name << "转账到" << to.name << "共计" << amount << "元!" << std::endl; } else { std::cout << from.name << "账户余额不足!" << std::endl; } to.mtx.unlock(); from.mtx.unlock(); } void test() { BankAccount accountA(1001, "张三", 1000); BankAccount accountB(1002, "李四", 5000); std::thread t1(bank_transfer, std::ref(accountA), std::ref(accountB), 200); std::thread t2(bank_transfer, std::ref(accountB), std::ref(accountA), 100); t1.join(); t2.join(); accountA.printBalance(); accountB.printBalance(); } int main() { test(); return 0; } #endif
通过运行上面的代码,我们发现程序出现了 “卡死” 的现象,这就是由于死锁导致。我们分析下代码的执行逻辑,在 bank_transfer
函数中,两个线程(A 和 B)会尝试同时锁住两个账户的互斥量(mtx
)。具体情况是:
- 线程 A 获取了
accountA
的锁,准备获取accountB
的锁。 - 线程 B 获取了
accountB
的锁,准备获取accountA
的锁。
由于两者同时等待对方的锁,这就导致了 死锁。至此,我们知道上述代码发生了什么问题。
2. 解决思路
明确了问题,怎么破解这个相互等待的问题?我们有两个解决思路:
第一个思路,我们发现 A 转账 B 和 B 转账 A 时,加锁的顺序是不一样的,如果改成一样的:
- 线程 A 获取了
accountA
的锁,准备获取accountB
的锁。 - 线程 B 获取了
account
A 的锁,准备获取account
B 的锁。
第二个思路,确保同时能够加锁 A 和 B,即:当线程 A 无法实现同时加锁 A 和 B 的时候,就全部释放锁。让其他线程有机会同时获得 A 和 B 的锁进行转账。这样也能够避免线程加锁 A 去等待 B,或者加锁 B 去等待 A 这种相互等待的情况。
2.1 解决方法一
#if 1 #include <iostream> #include <thread> #include <mutex> #include <chrono> struct BankAccount { BankAccount(size_t account_id, std::string name, double balance) : account_id(account_id), name(name), balance(balance) { } void printBalance() { std::cout << name << "账户余额为:" << balance << std::endl; } size_t account_id; std::string name; double balance; std::mutex mtx; }; void bank_transfer(BankAccount& from, BankAccount& to, double amount) { std::this_thread::sleep_for(std::chrono::seconds(1)); // 按照账户ID大小顺序加锁,小ID先加锁,大ID后加锁 // 无论从 A 还是 B 的角度,都能保证加锁、解锁的顺序一致。 if (from.account_id < to.account_id) { to.mtx.lock(); from.mtx.lock(); } else { from.mtx.lock(); to.mtx.lock(); } if (from.balance >= amount) { from.balance -= amount; to.balance += amount; std::cout << "从" << from.name << "转账到" << to.name << "共计" << amount << "元!" << std::endl; } else { std::cout << from.name << "账户余额不足!" << std::endl; } to.mtx.unlock(); from.mtx.unlock(); } void test() { BankAccount accountA(1001, "张三", 1000); BankAccount accountB(1002, "李四", 5000); std::thread t1(bank_transfer, std::ref(accountA), std::ref(accountB), 200); std::thread t2(bank_transfer, std::ref(accountB), std::ref(accountA), 100); t1.join(); t2.join(); accountA.printBalance(); accountB.printBalance(); } int main() { test(); return 0; } #endif
2.2 解决方法二
#if 1 #include <iostream> #include <thread> #include <mutex> #include <chrono> struct BankAccount { BankAccount(size_t account_id, std::string name, double balance) : account_id(account_id), name(name), balance(balance) { } void printBalance() { std::cout << name << "账户余额为:" << balance << std::endl; } size_t account_id; std::string name; double balance; std::mutex mtx; }; void bank_transfer(BankAccount& from, BankAccount& to, double amount) { std::this_thread::sleep_for(std::chrono::seconds(1)); // 使用 std::lock 来保证多个锁同时加锁成功,如果有一个加锁失败,则释放锁 std::lock(from.mtx, to.mtx); if (from.balance >= amount) { from.balance -= amount; to.balance += amount; std::cout << "从" << from.name << "转账到" << to.name << "共计" << amount << "元!" << std::endl; } else { std::cout << from.name << "账户余额不足!" << std::endl; } to.mtx.unlock(); from.mtx.unlock(); } void test() { BankAccount accountA(1001, "张三", 1000); BankAccount accountB(1002, "李四", 5000); std::thread t1(bank_transfer, std::ref(accountA), std::ref(accountB), 200); std::thread t2(bank_transfer, std::ref(accountB), std::ref(accountA), 100); t1.join(); t2.join(); accountA.printBalance(); accountB.printBalance(); } int main() { test(); return 0; } #endif
3. 实现源码
template <class _Lock0, class _Lock1> bool _Lock_attempt_small(_Lock0& _Lk0, _Lock1& _Lk1) { _Lk0.lock(); { _Unlock_one_guard<_Lock0> _Guard{_Lk0}; if (_Lk1.try_lock()) { _Guard._Lk_ptr = nullptr; return false; } } _STD this_thread::yield(); return true; } template <class _Lock0, class _Lock1> void _Lock_nonmember1(_Lock0& _Lk0, _Lock1& _Lk1) { while (_Lock_attempt_small(_Lk0, _Lk1) && _Lock_attempt_small(_Lk1, _Lk0)) {} } _EXPORT_STD template <class _Lock0, class _Lock1, class... _LockN> void lock(_Lock0& _Lk0, _Lock1& _Lk1, _LockN&... _LkN) { _Lock_nonmember1(_Lk0, _Lk1, _LkN...); }
- _Lock_nonmember1 调用 _Lock_attempt_small 函数尝试同时对多个锁进行加锁
- _Lock_attempt_small 函数中:
- 首先,对其中的任意一个锁进行加锁
- 然后,尝试对另外一个锁进行加锁,如果成功,则返回 false。_Lock_nonmember1 函数中的 while 循环退出。继续执行当前线程后续代码
- 如果加锁失败,则对第一个锁进行释放锁的操作。并且主动放弃剩余的时间片,等待操作系统再次调度执行当前线程,相当于等待一段时间,再重新尝试对两个锁进行加锁操作。