我正在编写一个代码来实现线程安全和线程不安全的死锁程序。如果一切正常,运行就没有问题。
#include <iostream>
#include <thread>
#include<vector>
#include <mutex>
using namespace std;
class Balance {
public:
int num;
explicit Balance(int num) : num(num) {};
std::mutex m;
};
void transfer_unsafe(Balance &from, Balance &to, int n) {
from.num -= n;
to.num += n;
}
void transfer(Balance &from, Balance &to, int n) {
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
std::lock(from.m, to.m);
from.num -= n;
to.num += n;
}
int main() {
auto test = [](const string &name, const auto func) {
auto start_time = std::chrono::high_resolution_clock::now();
Balance a{100};
Balance b{100};
std::cout << name << std::endl;
std::cout << "before " << a.num << " " << b.num << std::endl;
std::vector<std::thread> threads;
threads.reserve(100);
for (int i = 0; i < 1000; i++) {
threads.push_back(std::thread{func, std::ref(a), std::ref(b), 1});
}
for (int i = 0; i < 1000; i++) {
threads.push_back(std::thread{func, std::ref(b), std::ref(a), 1});
}
for (auto &t: threads) {
t.join();
}
std::cout << "after " << a.num << " " << b.num << std::endl;
auto end_time = std::chrono::high_resolution_clock::now();
std::cout << "time : " << (end_time - start_time).count() << std::endl;
};
test("unsafe", transfer_unsafe);
test("safe", transfer);
}
但是为什么它会导致核心转储?有没有死锁
我希望上面的程序打印出2个案例,但是
safe
部分不会给出任何答案。
正如 @Yksisarvinen 和 @G.M 在评论中指出的那样,您应该通过您创建的 unique_locks 锁定互斥锁:
//std::lock(from.m, to.m);
std::lock(lock1, lock2);
完成此修复后,您仍然通过
std::lock
创建两组具有相反(非等价)顺序的锁定互斥锁的线程。此函数不提供有关锁的特定顺序的任何保证,因此即使使用相似的参数顺序调用它也可能导致竞争锁定多个互斥体,并导致死锁。这是一个已知问题,C++17 解决方案是 std::scoped_lock
:
auto scope = std::scoped_lock(from.m, to.m);
这确保了无竞争的锁定顺序,从而消除了互斥锁上的死锁。
std::scoped_lock
保证使用某种死锁避免算法。
另请注意,我们不再需要使用 std::unique_lock
。使用它们不会有什么害处,只要它们是作为参数而不是原始参数传递的,但不必要的冗余将成为未来错误和错误的根源。
在不关心死锁的情况下,std::lock_guard
可用于锁定多个互斥体,其语法与std::scoped_lock
类似。
最后,考虑使用 C++20
[std::jthread
]3 而不是 std::thread
。它在销毁时自动加入(作用域退出)。与 C++11 线程类相比,C++ 20 jthread 类还有进一步的改进,但这就是 OT。