用于防止代码死锁的锁定策略和技术

时间:2011-05-16 02:16:16

标签: c++ design-patterns locking deadlock

防止代码死锁的常见解决方案是确保锁定顺序以通用方式发生,而不管哪个线程正在访问资源。

例如给定线程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...

我的问题是在编码中使用了哪些其他技术,模式或常用做法来保证死锁预防?

5 个答案:

答案 0 :(得分:30)

您描述的技术并不常见:它是一种已被证明可以一直工作的技术。在使用C ++编写线程代码时,您应该遵循一些其他规则,其中最重要的可能是:

  • 在调用虚拟函数时不要保持锁定:即使在编写代码时,您知道将调用哪个函数以及它将执行什么操作,代码也会发展,并且虚拟函数可以被覆盖,因此最终,不会知道它的作用以及它是否会接受任何其他锁定,这意味着您将失去保证的锁定顺序
  • 注意竞争条件:在C ++中,当线程之间共享给定数据并且您不使用某种同步时,没有任何内容可以告诉您。其中一个例子是几天前发布在C ++休息室的聊天,作为一个例子(本帖末尾的代码):只是尝试同步 else 碰巧在附近并不意味着你的代码正确同步。
  • 尝试隐藏异步行为:您通常 更好地隐藏您的软件架构中的并发性,这样大多数调用代码都不关心是否有线程存在或不。它使架构更易于使用 - 特别是对于那些不习惯并发的人。

我可以继续一段时间,但根据我的经验,最简单使用线程的方法是使用每个可能使用代码的人都熟知的模式,例如生产者/消费者模式:它易于解释,您只需要一个工具(队列)来允许您的线程相互通信。毕竟,两个线程彼此同步的唯一原因是允许它们进行通信。

更一般的建议:

  • 在你使用锁定进行并发编程的经验之前,不要尝试无锁编程 - 这是一种简单的方法,可以让你的脚脱落,或遇到非常奇怪的错误。
  • 将共享变量的数量和访问这些变量的次数减少到最低限度。
  • 不要指望两个事件总是以相同的顺序发生,即使你看不到任何方式扭转顺序。
  • 更一般地说:不要指望时间 - 不要认为某项任务应该总是花费一定的时间。

以下代码将失败:

#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();
}

答案 1 :(得分:14)

锁定的一致排序几乎是避免死锁的第一个也是最后一个词。

有一些相关技术,例如无锁编程(没有线程等待锁定,因此不存在循环的可能性),但这实际上只是“避免不一致锁定顺序”规则的特殊情况 - - 即它们通过避免所有锁定来避免不一致的锁定。不幸的是,无锁编程有其自身的问题,所以它也不是灵丹妙药。

如果你想扩大范围,有一些方法可以在发生死锁时检测死锁(如果由于某种原因你无法设计你的程序以避免它们),以及在它们发生时打破死锁的方法(例如,总是通过超时锁定,或者强制其中一个死锁线程使其Lock()命令失败,或者甚至只是通过杀死其中一个死锁线程);但我认为他们都非常不能简单地确保首先不会发生死锁。

(顺便说一句,如果你想要一个自动的方法来检查你的程序是否有潜在的死锁,请查看valgrind的helgrind工具。它将监视代码的锁定模式并通知你任何不一致 - 非常有用)

答案 2 :(得分:6)

另一种技术是事务编程。这虽然不常见,因为它通常涉及专门的硬件(大部分目前仅在研究机构中)。

每个资源都会跟踪来自不同线程的修改。提交所有资源(它正在使用)的第一个线程获胜所有其他线程(使用这些资源)将回滚以再次使用新提交状态的资源。

阅读主题的简单起点是transactional memory

答案 3 :(得分:3)

虽然不是你提到的已知序列解决方案的替代品,但Andrei Alexandrescu写了一些关于编译时检查的技术,即通过预期的机制完成锁的获取。见http://www.informit.com/articles/article.aspx?p=25298

答案 4 :(得分:3)

您在询问设计级别,但我会添加一些较低级别的编程实践。

  • 将每个函数(方法)归类为阻止非阻塞或具有未知的阻止行为。
  • 阻止功能是获取锁定或调用慢速系统调用(实际上意味着它执行I / O)或调用阻止的函数功能
  • 函数是否保证是非阻塞的是该函数规范的一部分,就像它的前提条件和异常安全程度一样。因此必须记录在案。在Java中我使用注释;在使用Doxygen记录的C ++中,我在函数的标题注释中使用了一个forumalic短语。
  • 考虑将一个非指定的函数调用为非阻塞,同时保持锁定是危险的。
  • 重构此类危险代码以消除危险或将危险集中在一小段代码中(可能在其自身的功能范围内)。
  • 对于剩余的危险代码,提供一个非正式的证明,证明代码在代码的注释中实际上并不危险。