防止代码死锁的锁定策略和技术[英] Locking strategies and techniques for preventing deadlocks in code

本文是小编为大家收集整理的关于防止代码死锁的锁定策略和技术的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

防止代码中的僵局的常见解决方案是确保以通用方式出现锁定顺序,而不管线程访问资源如何.

例如,给定的线程T1和T2,其中T1访问资源A,然后B和T2访问资源B,然后访问A. 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 ++中,当线程之间共享给定的数据何时,您不会在其上使用某种同步.一个例子是在几天前的C ++休息室中发布的,luc,作为此示例(本文结尾处的代码):只是尝试同步 else 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();
}

其他推荐答案

在避免死锁时,锁定的一致排序几乎是第一个和最后一句话.

有相关的技术,例如无锁编程(其中没有线程在锁上等待,因此没有一个周期的可能性),但这实际上只是"避免不一致的锁定顺序"规则的特殊情况 - - 即,通过避免所有锁定,它们避免锁定不一致.不幸的是,无锁编程有自己的问题,因此也不是灵丹妙药.

如果您想稍微扩大范围,则有一些方法在发生僵局时(如果出于某种原因,您无法设计程序以避免它们),以及在发生僵局时破坏僵局的方法(例如,始终始终用超时锁定,或者强迫其中一个僵局的锁定命令()命令失败,甚至仅通过杀死其中一个僵局);但是我认为它们都比仅仅确保僵局不可能首先发生.

(顺便说一句,如果您想要一种自动化方法来检查您的程序是否有潜在的僵局,请查看Valgrind的Helgrind工具.它将监视您的代码的锁定模式,并将任何矛盾之处通知您 - 非常有用)

)

)

其他推荐答案

另一种技术是交易编程.尽管这通常不是很常见,因为它通常涉及专业硬件(目前大部分仅在研究机构中).

每个资源都跟踪来自不同线程的修改.提交所有资源(正在使用)更改的第一个线程赢得所有其他线程(使用这些资源)将重新回滚以再次尝试使用新的承诺状态中的资源.

阅读该主题的一个简单的起点是交易记忆..

本文地址:https://www.itbaoku.cn/post/627611.html

问题描述

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.

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.

Problematic situation:

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...

Possible Solution:

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?

推荐答案

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:

  • 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.

More general advice:

  • Don't try your hand at lock-free programming until you've had experience with concurrent programming using locks - it's an easy way to blow your foot off, or run into very strange bugs.
  • Reduce the number of shared variables and the number of times those variables are accessed to a bare minimum.
  • Don't count on two events always occurring in the same order, even if you can't see any way of them reversing order.
  • More generally: don't count on timing - don't think a given task should always take a given amount of time.

The following code will fail:

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

其他推荐答案

Consistent ordering of locking is pretty much the first and last word when it comes to deadlock avoidance.

There are related techniques, such as lockless programming (where no thread ever waits on a lock, and thus there is no possibility of a cycle), but that's really just a special case of the "avoid inconsistent locking order" rule -- i.e. they avoid inconsistent locking by avoiding all locking. Unfortunately, lockless programming has its own issues, so it's not a panacea either.

If you want to broaden the scope a bit, there are methods for detecting deadlocks when they do occur (if for some reason you can't design your program to avoid them), and ways for breaking deadlocks when they do occur (e.g. by always locking with a timeout, or by forcing one of the deadlocked threads to have their Lock() command fail, or even just by killing one of the deadlocked threads); but I think they are all pretty inferior to simply making sure deadlocks cannot happen in the first place.

(btw if you want an automated way to check whether your program has potential deadlocks in it, check out valgrind's helgrind tool. It will monitor your code's locking patterns and notify you of any inconsistencies -- very useful)

其他推荐答案

Another technique is transactional programming. This though is not very common as it usually involves specialized hardware (most of it currently only in research institutions).

Each resource keeps track of modifications from different threads. The first thread to commit changes to all resources (it is using) wins all other thread (using those resources) get rolled back to try again with the resources in the new committed state.

A simplistic starting point for reading on the subject is transactional memory.