防止代码死锁的锁定策略和技术
防止代码中死锁的常见解决方案是确保锁定序列以共同的方式发生,而不管哪个线程正在访问资源.
The common solution to preventing deadlock in code is to make sure the sequence of locking occur in a common manner regardless of which thread is accessing the resources.
例如给定线程 T1 和 T2,其中 T1 访问资源 A,然后 B 和 T2 访问资源 B,然后 A.按需要的顺序锁定资源会导致死锁.简单的解决方案是先锁定 A 再锁定 B,无论特定线程使用资源的顺序如何.
For example given threads T1 and T2, where T1 accesses resource A and then B and T2 accesses resource B and then A. Locking the resources in the order they are needed causes a dead-lock. The simple solution is to lock A and then lock B, regardless of the order specific thread will use the resources.
问题情况:
Thread1 Thread2
------- -------
Lock Resource A Lock Resource B
Do Resource A thing... Do Resource B thing...
Lock Resource B Lock Resource A
Do Resource B thing... Do Resource A thing...
可能的解决方案:
Thread1 Thread2
------- -------
Lock Resource A Lock Resource A
Lock Resource B Lock Resource B
Do Resource A thing... Do Resource B thing...
Do Resource B thing... Do Resource A thing...
我的问题是在编码中使用了哪些其他技术、模式或常见做法来保证防止死锁?
My question is what other techniques, patterns or common practices are used in coding to guarantee dead lock prevention?
推荐答案
您描述的技术不仅很常见:它还是一种已被证明一直有效的技术.不过,在用 C++ 编写线程代码时,您还应该遵循一些其他规则,其中最重要的可能是:
The technique you describe isn't just common: it's the one technique that has been proven to work all the time. There are a few other rules you should follow when coding threaded code in C++, though, among which the most important may be:
- 在调用虚函数时不要持有锁:即使在编写代码时你知道将调用哪个函数以及它会做什么,代码会不断发展,并且虚函数会被覆盖,所以最终,你不会知道它做了什么以及它是否需要任何其他锁,这意味着你将失去你保证的锁定顺序
- 注意竞争条件:在 C++ 中,当给定的数据在线程之间共享并且您不对其使用某种同步时,什么都不会告诉您.几天前,Luc 在 SO chat 的 C++ Lounge 中发布了一个示例,作为示例(本文末尾的代码):只是尝试同步else碰巧在附近并不意味着您的代码已正确同步.
- 尝试隐藏异步行为:您通常在软件架构中更好地隐藏并发性,这样大多数调用代码就不会关心那里是否有线程或不.它使架构更易于使用 - 特别是对于那些不习惯并发的人.
- don't hold a lock when calling a virtual function: even if at the time you're writing your code you know which function will be called and what it will do, code evolves, and virtual functions are there to be overridden, so ultimately, you won't know what it does and whether it will take any other locks, meaning you will lose your guaranteed order of locking
- watch out for race conditions: in C++, nothing will tell you when a given piece of data is shared between threads and you don't use some kind of synchronization on it. One example of this was posted in the C++ Lounge on SO chat a few days ago, by Luc, as an example of this (code at the end of this post): just trying to synchronize on something else that happens to be in the neighborhood doesn't mean your code is correctly synchronized.
- try to hide asynchronous behavior: you're usually better hiding your concurrency in your software's architecture, such that most calling code won't care whether there's a thread there or not. It makes the architecture easier to work with - especially for some-one who isn't used to concurrency.
我可以继续一段时间,但根据我的经验,使用线程的最简单方法是使用每个可能使用代码的人都熟知的模式,例如生产者/消费者模式:很容易解释,你只需要一个工具(一个队列)就可以让你的线程相互通信.毕竟,两个线程相互同步的唯一原因是允许它们进行通信.
I could go on for a while, but in my experience, the easiest way to work with threads is using patterns that are well-known to everyone who might work with the code, such as the producer/consumer pattern: it's easy to explain and you only need one tool (a queue) to allow your threads to communicate with each other. After all, the only reason for two threads to be synchronized with each other, is to allow them to communicate.
更一般的建议:
- 在您有使用锁进行并发编程的经验之前,不要尝试无锁编程 - 这是一种让您大开眼界或遇到非常奇怪的错误的简单方法.
- 将共享变量的数量和这些变量的访问次数减少到最低限度.
- 不要指望两个事件总是以相同的顺序发生,即使您看不到它们的任何颠倒顺序.
- 更一般地说:不要指望时间 - 不要认为给定的任务应该总是花费给定的时间.
#include <thread>
#include <cassert>
#include <chrono>
#include <iostream>
#include <mutex>
void
nothing_could_possibly_go_wrong()
{
int flag = 0;
std::condition_variable cond;
std::mutex mutex;
int done = 0;
typedef std::unique_lock<std::mutex> lock;
auto const f = [&]
{
if(flag == 0) ++flag;
lock l(mutex);
++done;
cond.notify_one();
};
std::thread threads[2] = {
std::thread(f),
std::thread(f)
};
threads[0].join();
threads[1].join();
lock l(mutex);
cond.wait(l, [done] { return done == 2; });
// surely this can't fail!
assert( flag == 1 );
}
int
main()
{
for(;;) nothing_could_possibly_go_wrong();
}
相关文章