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

发布于 2024-11-07 02:24:51 字数 863 浏览 5 评论 0原文

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

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

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

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

念﹏祤嫣 2024-11-14 02:24:51

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

  • 调用虚函数时不要持有锁:即使当时当你编写代码时,你知道哪个函数将被调用以及它会做什么,代码会不断发展,并且虚函数会被重写,所以最终,你不会知道它的作用,并且是否需要任何其他锁,这意味着你会输您保证的锁定顺序
  • 注意竞争条件:在 C++ 中,当给定的数据片段在线程之间共享并且您没有对其使用某种同步时,没有任何信息会告诉您。 Luc 几天前在 SO 聊天室的 C++ Lounge 中发布了一个这样的示例,作为一个示例(代码在本文末尾):只是尝试同步其他东西恰好在附近并不意味着您的代码已正确同步。
  • 尝试隐藏异步行为:您通常最好将并发性隐藏在软件架构中,这样大多数调用代码就不会关心那里是否有线程。它使架构更容易使用——特别是对于那些不习惯并发的人来说。

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

更一般的建议:

  • 在您拥有使用锁的并发编程经验之前,不要尝试无锁编程 - 这是一种很容易让您摔倒的方法,或者遇到非常奇怪的错误。
  • 将共享变量的数量和访问这些变量的次数减少到最低限度。
  • 不要指望两个事件总是以相同的顺序发生,即使您看不到它们有任何相反的顺序。
  • 更一般地说:不要指望时间——不要认为给定的任务应该总是花费给定的时间。

下面的代码将会失败:

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

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();
}
梦里人 2024-11-14 02:24:51

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

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

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

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

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)

带刺的爱情 2024-11-14 02:24:51

另一种技术是事务性编程。但这并不常见,因为它通常涉及专用硬件(其中大多数目前仅在研究机构中)。

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

阅读该主题的一个简单起点是事务内存

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.

む无字情书 2024-11-14 02:24:51

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

While not an alternative to the known-sequence solution you mention, Andrei Alexandrescu wrote about some techniques for compile time checks that acquisition of locks is done through the intended mechanisms. See http://www.informit.com/articles/article.aspx?p=25298

木槿暧夏七纪年 2024-11-14 02:24:51

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

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

You are asking about the design level, but I'll add some lower level, programming practices.

  • Classify each function (method) as blocking, non-blocking or having unknown blocking behaviour.
  • A blocking function is a function that acquires a lock, or calls a slow system call (which in practice means it does I/O), or calls a blocking function.
  • Whether a function is guaranteed to be non-blocking is part of the specification of that function, just like its preconditions and its degree of exception safety. It must therefore be documented as such. In Java I use an annotation; in C++ documented using Doxygen I'd use a forumalic phrase in the header commentary for the function.
  • Consider calling a function that is not specified to be non-blocking while holding a lock to be dangerous.
  • Refactor such dangerous code to eliminate the danger or to concentrate the danger into a small section of code (perhaps within its own function).
  • For the remaining dangerous code, provide an informal proof that the code is not actually dangerous in the commentary of the code.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文