【问题标题】:Threads and simple Dead lock cure线程和简单的死锁治疗
【发布时间】:2009-12-12 07:04:36
【问题描述】:

在使用互斥锁和信号量处理线程(特别是在 C++ 中)时,是否有一个简单的经验法则可以避免死锁并进行干净的同步?

【问题讨论】:

    标签: c++ synchronization deadlock mutex semaphore


    【解决方案1】:

    一个很好的简单经验法则是始终以一致的可预测顺序从应用程序的任何位置获取锁。例如,如果您的资源有名称,请始终按字母顺序锁定它们。如果他们有数字 id,请始终从最低到最高锁定。确切的顺序或标准是任意的。关键是要保持一致。这样你就永远不会陷入僵局。例如。

    1. 线程 1 锁定资源 A
    2. 线程 2 锁定资源 B
    3. 线程 1 等待获取 B 上的锁
    4. 线程 2 等待获取 A 上的锁
    5. 死锁

    如果您遵循上述经验法则,上述情况永远不会发生。如需更详细的讨论,请参阅Wikipedia entry on the Dining Philosophers problem

    【讨论】:

      【解决方案2】:
      1. 如果可能,请设计您的代码,这样您就不必一次锁定超过一个互斥量/信号量。
      2. 如果不可能,请确保始终以相同的顺序锁定多个互斥体/信号量。因此,如果代码的一部分锁定了互斥量 A 然后获取信号量 B,请确保代码的其他部分没有获取信号量 B 然后锁定互斥量 A。

      【讨论】:

      • +1 正是我要给出的答案。我听过的最好的并发编程建议。
      【解决方案3】:

      尽量避免获取一个锁并尝试获取另一个锁。这可能导致循环依赖并导致死锁。 如果这是不可避免的,那么至少获取锁的顺序应该是可预测的。

      使用RAII(确保在异常情况下也能正确释放锁)

      【讨论】:

      • 这听起来不太实际。在很多情况下,您一次需要持有多个锁。如果您使用粒度锁而不是一个全局锁,则尤其如此。
      • 一种更实用的方法是使用允许您同时获取一组锁的 API。如果您知道需要锁 A、B 和 C,请调用 lock(A, B, C),该函数将尝试锁定所有这些,并以正确的顺序执行。你可以自己写一个这样的函数,以正确的顺序集中获取锁的代码。
      • 为什么不实用?在进入另一个组件(通过尝试获取另一个锁)之前释放互斥锁调用更安全。 “等待”是死锁的基本原因之一
      • aJ:很多问题都需要你获得多个锁。一次只持有一把锁是不切实际的。
      • 这样设计并且仍然具有性能是不切实际的。虽然可能有一些方法让我无法理解,但用更少的锁做更多事情的通常方法是让锁更宽。这很好,直到你有很多线程,然后它们经常阻塞并且性能下降。全局锁是解决死锁的方法,但你是性能的死亡。
      【解决方案4】:

      没有简单的死锁解决方法。

      按约定的顺序获取锁:如果所有调用都获取 A->B->C,则不会发生死锁。仅当两个线程之间的锁定顺序不同(一个获取 A->B,第二个获取 B->A)时,才会发生死锁。

      实际上很难在内存中的任意对象之间选择顺序。在一个简单的琐碎项目上是可能的,但在有许多个人贡献者的大型项目上是非常困难的。部分解决方案是通过对锁进行排名来创建层次结构。模块 A 中的所有锁的等级为 1,模块 B 中的所有锁的等级为 2。当持有等级 1 的锁时,可以获得等级为 2 的锁,反之则不行。当然,您需要一个围绕锁定原语的框架来跟踪和验证排名。

      【讨论】:

      • 在 C++ 中,选择订单非常简单。只需按内存地址排序。在 C# 或 Java 中,这比较棘手,因为对象的地址不会暴露给程序员,并且可能在对象的生命周期内发生变化。
      • 我希望事情能这么简单...如果您处于代码具有锁定对象列表的罕见位置 a、b、c 是的,您可以比较地址。但是大多数时候 a 被锁定在函数 f() 中,b 被锁定在 g() 中,c 被锁定在 din h() 中,并且这些函数都不知道另一个。在应用程序中假设所有锁都将在块中获取,知道所有锁将是什么以及对每个锁的引用,这是不现实的。
      【解决方案5】:

      确保其他人讨论过的顺序的一种方法是按照他们的内存地址定义的顺序获取锁。如果在任何时候,您尝试获取本应在序列中较早的锁,则释放所有锁并重新开始。

      只需稍做工作,就可以使用围绕系统原语的一些包装类几乎自动完成此操作。

      【讨论】:

        【解决方案6】:

        没有实用的治愈方法。具体来说,没有办法简单地测试代码是否同步正确,或者让你的程序员遵守带绿色 V 的绅士规则。

        没有办法正确地测试多线程代码,因为程序逻辑可能依赖于锁获取的时间,因此,执行与执行之间存在差异,从而使 QA 的概念失效。

        我会说

        • 更喜欢仅将线程用作多核机器的性能优化
        • 仅当您确定需要此性能时才优化性能
        • 您可以使用线程来简化程序逻辑,但前提是您绝对确定自己在做什么。要格外小心,所有锁都仅限于一小段代码。不要让任何新手靠近此类代码。
        • 切勿在关键任务系统中使用线程,例如驾驶飞机或操作危险机器
        • 在所有情况下,由于调试和 QA 成本较高,线程很少具有成本效益

        如果您决定做线程或维护现有代码库:

        • 将所有锁限制在对原语进行操作的小而简单的代码中
        • 避免函数调用或将程序流转移到无法立即看到在锁定下执行的事实的地方。此功能将由未来的作者更改,在不受您控制的情况下扩大您的锁定范围。
        • 获取对象内部的锁以减少锁定范围,使用您自己的线程安全接口包装非线程安全的第 3 方对象。
        • 在锁定执行时从不发送同步通知(回调)
        • 仅使用 RAII 锁,以减少在思考“我们如何才能从这里退出”时的认知负荷,例如异常等。

        关于如何避免多线程的几句话。

        单线程设计通常涉及程序组件提供的一些心跳功能,并在循环中调用(称为心跳循环),当被调用时,所有组件都有机会完成下一个工作并再次交出控制权。算法学家喜欢将组件内的“循环”视为组件中的“循环”,它们将变成状态机,以识别调用时应该做的下一件事是什么。状态最好作为各个对象的成员数据进行维护。

        【讨论】:

        • 在编写应用程序之后尝试将多线程绑定到应用程序是失败的秘诀。如果您要使用线程,请正确执行,并从第一天起将其纳入应用程序的架构。否则,您遇到所有令人讨厌的死锁和竞争条件。老实说,除非你还生活在 80 年代,否则任何假装 CPU 密集型(或只需要响应)的应用程序都无法真正避免线程化。
        【解决方案7】:

        有很多简单的“僵局解决方法”。但是没有一个是容易应用和普遍工作的。

        最简单的当然是“永远不会有多个线程”。

        假设您有一个多线程应用程序,仍然有许多解决方案:

        您可以尝试最小化共享状态和同步。两个并行运行且从不交互的线程永远不会死锁。死锁仅在多个线程尝试访问同一资源时发生。他们为什么这样做?可以避免吗?是否可以对资源进行重组或划分,例如,一个线程可以写入,而其他线程异步传递所需的数据?

        也许可以复制资源,让每个线程都有自己的私有副本可以使用?

        正如所有其他答案已经提到的那样,如果当您尝试获取锁时,请以全局一致的顺序进行。为了简化这一点,您应该尝试确保线程将需要的所有锁都作为单个操作获取。如果一个线程需要获取锁A、B和C,它不应该在不同的时间和不同的地方进行三个lock()调用。您会感到困惑,并且您将无法跟踪线程持有哪些锁,以及它尚未获取哪些锁,然后您会弄乱顺序。如果你可以一次获取你需要的所有锁,那么你可以将它分解到一个单独的函数调用中,该函数调用获取N个锁,并以正确的顺序执行以避免死锁。

        还有更雄心勃勃的方法:CSP 之类的技术使线程变得非常简单,并且很容易证明是正确的,即使有数千个并发线程也是如此。但这需要您以与以往截然不同的方式构建程序。

        Transactional Memory 是另一种有前途的选择,并且可能更容易集成到传统程序中。但生产质量的实现仍然非常罕见。

        【讨论】:

          【解决方案8】:

          阅读Deadlock: the Problem and a Solution.

          “避免死锁的常见建议是始终以相同的顺序锁定两个互斥锁:如果您始终在互斥锁 B 之前锁定互斥锁 A,那么您将永远不会死锁。有时这很简单,因为互斥锁服务于不同的目的,但其他时候并不那么简单,例如当互斥体各自保护同一类的单独实例时”。

          【讨论】:

            【解决方案9】:

            如果你想攻击死锁的可能性,你必须攻击死锁存在的 4 个关键条件之一。

            发生死锁的 4 个条件是: 1. 互斥——一次只能有一个线程进入临界区。 2. Hold and Wait——线程不会释放他获得的资源,只要他没有完成他的工作,即使其他资源不可用。 3. 无抢占 - 一个线程不具有高于其他线程的优先级。 4. 资源循环 - 必须有一个线程循环链等待来自其他线程的资源。

            最容易攻击的条件是资源循环,确保没有循环是可能的。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-06-14
              • 2013-04-24
              相关资源
              最近更新 更多