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

Posted

技术标签:

【中文标题】防止代码死锁的锁定策略和技术【英文标题】:Locking strategies and techniques for preventing deadlocks in code 【发布时间】:2011-05-16 02:16:15 【问题描述】:

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

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

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

【问题讨论】:

【参考方案1】:

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

调用虚函数时不要持有锁:即使在您编写代码时,您知道将调用哪个函数以及它将做什么,代码会不断发展,而虚函数也会不断变化。那里会被覆盖,所以最终,您不会知道它的作用以及它是否会占用任何其他锁,这意味着您将失去保证的锁定顺序 注意竞争条件:在 C++ 中,什么时候给定的数据在线程之间共享并且您没有对其使用某种同步,什么时候都不会告诉您。几天前,Luc 在 SO chat 上的 C++ Lounge 上发布了一个示例(本文末尾的代码):只是尝试同步某件事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();

【讨论】:

“你不会知道它做了什么以及它是否会占用任何其他锁”,除非基类函数的规范是该方法永远不会获取锁。 “该规范必须以某种方式强制执行”:可能与规范的任何方面相同的声明。在实践中几乎没有强制执行。有些是通过审核来检查的,大多数是通过测试来检查的。 @Raedwald 我的意思是,你不能总是依赖基类函数的规范来反映函数的实际作用(和.或函数的其他版本)做)。 大部分通过测试检查是正确的,遗憾的是,这意味着大多数问题没有及时发现以防止损坏。这并不意味着不应该明确指定基类函数 - 它只是意味着在实践中不一定依赖这些规范。【参考方案2】:

在避免死锁方面,一致的锁定顺序几乎是第一个也是最后一个词。

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

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

(顺便说一句,如果您想要一种自动化的方式来检查您的程序中是否存在潜在的死锁,请查看 valgrind 的 helgrind 工具。它会监控您的代码的锁定模式并通知您任何不一致的地方——非常有用)

【讨论】:

无锁编程不是一种避免不一致锁定的技术:它是一个旨在创建真正可扩展的并发算法的研究领域。无锁编程的唯一问题是它很难,所以不推荐给没有太多并发编程经验的人。 @rlc 和@Jeremy:“无锁”编程甚至不是真正的无锁。它只是使用在硬件、缓存一致性协议和处理器间通信中实现的锁定。如果没有某种锁,那么深受喜爱的比较交换指令就无法工作。 @rlc 很公平——但我没有另外声明,我只是说这是一种相关技术。 @Zan 在保证线程不会无限期阻塞的意义上,它是无锁的,因此不会发生死锁。 @ZanLynx true:处理器将相互同步,并且在 x86 架构中,LOCK 引脚被断言,这两者都需要大量成本。但是,锁本身是使用相同的硬件级同步机制实现的,并且在软件级别上,虽然存在同步点,但至少有一个线程将始终生成锁,因此没有锁。前进的进步。***有一个很好的定义here【参考方案3】:

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

每个资源都跟踪来自不同线程的修改。第一个对所有资源(它正在使用)提交更改的线程将赢得所有其他线程(使用这些资源),然后回滚以使用处于新提交状态的资源重试。

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

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

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

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

【讨论】:

以上是关于防止代码死锁的锁定策略和技术的主要内容,如果未能解决你的问题,请参考以下文章

通过防止在进程中查询来避免 SSAS 中的死锁

死锁处理策略和死锁预防

具有层次锁定的 RW 锁定

在 SQL 中使用双重检查锁定死锁

50 怎么防止死锁?

50 怎么防止死锁?