用于防止代码中死锁的锁定策略和技术
Locking strategies and techniques for preventing deadlocks in code
代码死锁的常见解决方案是确保锁定顺序以通用方式发生,无论哪个线程正在访问资源。
例如,给定线程 T1 和 T2,其中 T1 访问资源 A,然后 B 和 T2 访问资源 B,然后访问 A。 按所需顺序锁定资源会导致死锁。简单的解决方案是锁定 A,然后锁定 B,无论特定线程使用资源的顺序如何。
问题情况:
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...
我的问题是,编码中使用了哪些其他技术、模式或常见做法来保证防止死锁?
你描述的技术不仅很常见:它是一种已被证明一直有效的技术。但是,在C++中编写线程代码时,还应遵循其他一些规则,其中最重要的可能是:
- 调用虚函数时
- 不要持有锁:即使在编写代码时,您也知道将调用哪个函数以及它将做什么,代码会演变,并且虚拟函数会被覆盖,所以最终,您将不知道它做什么以及它是否会接受任何其他锁, 这意味着您将失去保证的锁定顺序
- 注意争用条件:在C++中,当给定的数据片段在线程之间共享并且您没有对其使用某种同步时,没有任何内容会告诉您。几天前,Luc 在 SO chat 的 C++ 休息室发布了一个例子(本文末尾的代码):只是尝试同步碰巧在附近的其他东西并不意味着你的代码正确同步。
- 尝试隐藏异步行为:通常最好在软件体系结构中隐藏并发性,这样大多数调用代码就不会关心那里是否有线程。它使体系结构更易于使用 - 特别是对于不习惯并发的人。
我可以继续一段时间,但根据我的经验,使用线程的最简单方法是使用每个可能使用代码的人都知道的模式,例如生产者/消费者模式:它很容易解释,你只需要一个工具(一个队列)来允许你的线程相互通信。毕竟,两个线程相互同步的唯一原因是允许它们进行通信。
更多一般建议:
- 在你有使用锁的并发编程经验之前,不要尝试无锁编程 - 这是一种简单的方法,可以让你的脚掉,或者遇到非常奇怪的错误。
- 将共享变量的数量和访问这些变量的次数减少到最低限度。
- 不要指望两个事件总是以相同的顺序发生,即使你看不到它们逆转顺序的任何方式。
- 更一般地说:不要指望时间 - 不要认为给定的任务应该总是花费给定的时间。
以下代码将失败:
#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();
}
避免死锁方面,一致的锁定顺序几乎是第一个也是最后一个词。
有相关的技术,例如无锁编程(其中没有线程在锁上等待,因此没有循环的可能性),但这实际上只是"避免不一致的锁定顺序"规则的特例 - 即它们通过避免所有锁定来避免不一致的锁定。 不幸的是,无锁编程有其自身的问题,因此它也不是灵丹妙药。
如果你想扩大范围,有一些方法可以在死锁发生时检测死锁(如果由于某种原因你不能设计你的程序来避免它们),以及在死锁发生时打破死锁的方法(例如,通过始终锁定超时,或者强制其中一个死锁线程的 Lock() 命令失败, 甚至只是通过杀死其中一个死锁线程);但我认为它们都不如简单地确保首先不会发生死锁。
(顺便说一句,如果你想要一种自动化的方式来检查你的程序是否有潜在的死锁,请查看Valgrind的Helgrind工具。 它将监视代码的锁定模式并通知您任何不一致之处 - 非常有用)
另一种技术是事务性编程。这并不常见,因为它通常涉及专门的硬件(其中大部分目前仅在研究机构中)。
每个资源都会跟踪来自不同线程的修改。第一个将更改提交到所有资源(它正在使用)的线程将赢得所有其他线程(使用这些资源)将回滚以再次尝试使用处于新提交状态的资源。
阅读该主题的一个简单起点是事务性记忆。
虽然不是你提到的已知序列解决方案的替代品,但Andrei Alexandrescu写了一些编译时检查技术,即锁的获取是通过预期的机制完成的。 请参阅 http://www.informit.com/articles/article.aspx?p=25298
您询问的是设计级别,但我将添加一些较低级别的编程实践。
- 将每个功能(方法)分类为阻塞、非阻塞或具有未知阻塞行为。 阻塞函数
- 是获取锁或调用慢速系统调用(实际上意味着它执行 I/O)或调用阻塞函数的函数。
- 一个函数是否保证是非阻塞的,是该函数规范的一部分,就像它的前提条件和异常安全程度一样。因此,必须这样记录。在Java中,我使用注释;在使用 Doxygen 记录C++中,我会在函数的标题注释中使用论坛短语。
- 考虑在保持锁时调用未指定为非阻塞的函数是危险的。
- 重构此类危险代码以消除危险或将危险集中到一小段代码中(可能在其自己的函数中)。
- 对于剩余的危险代码,请在代码的注释中提供非正式证明,证明代码实际上并不危险。
- 如何找到锁定Linux futex的C++行
- G锁定铸造到基础上会释放模拟行为
- C++17中的并行执行策略
- 如何检查线程是否锁定
- 如何在C++中找到active directory中禁用和锁定的窗口帐户
- 我应该在锁定TBitmap画布后解锁它吗
- 运行时执行策略不同
- 编译器上的策略数据结构不起作用
- 我应该在简单的策略游戏中为各个派系使用类吗 - C++
- 给定一个C++嵌套的私有结构类型,是否有从文件范围静态函数访问它的策略
- C++ 11 中的锁定是否保证访问数据的新鲜度?
- 在两个线程上读/写 64 位,无互斥/锁定/原子
- 没有执行策略的 std::transform_reduce 是可移植的吗?
- C++ 运算符修改/元编程策略,用于不那么冗长的语法
- 如何在实时应用程序中锁定线程
- 在 lambda 中锁定 std::shared_ptr 的复制操作
- 使用简单两相锁定的并发程序
- C++中用于嵌套循环的OpenMP编程的锁定策略
- c++中原子的乐观锁定策略和排序
- 用于防止代码中死锁的锁定策略和技术