在处理线程(特别是在C++中)时,使用互斥锁和信号量,有没有简单的经验法则可以避免死锁,并实现良好的同步?
在处理线程(特别是在C++中)时,使用互斥锁和信号量,有没有简单的经验法则可以避免死锁,并实现良好的同步?
没有简单的死锁解决方案。
按约定顺序获取锁:如果所有调用都按照 A->B->C 的顺序获取锁,则不会发生死锁。只有当两个线程之间的锁定顺序不同时(一个获取 A->B,第二个获取 B->A)才会发生死锁。
在实践中,很难选择内存中任意对象的顺序。在一个简单的琐碎项目上是可能的,但在许多个人贡献的大型项目上非常困难。部分解决方案是通过对锁进行排名来创建层次结构。模块 A 中的所有锁都具有等级 1,模块 B 中的所有锁都具有等级 2。当持有等级 1 的锁时,可以获取等级 2 的锁,但反之则不行。当然,您需要一个围绕锁定原语的框架来跟踪和验证排名。
避免死锁的常见建议是始终以相同的顺序锁定两个互斥量:如果您总是在互斥量A之前锁定互斥量B,则永远不会发生死锁。有时这很简单,因为互斥量用于不同的目的,但其他情况则不是那么简单,例如当互斥量分别保护同一类的不同实例时。
确保其他人所谈论的顺序的一种方法是按照它们的内存地址定义锁的顺序来获取锁。如果在任何时候,您尝试获取应该早于序列中的先前锁定的锁,则会释放所有锁定并重新开始。
通过一些包装系统原语的包装器类,几乎可以自动完成这项工作。
没有实际的解决方法。具体来说,没有简单地测试代码是否同步正确的方法,也没有让程序员遵守绿色V字绅士规则的方法。
无法正确测试多线程代码,因为程序逻辑可能依赖于锁获取的时间,因此从执行到执行可能会有所不同,从而使QA的概念失效。
我建议:
如果您决定使用线程或维护现有的代码库:
关于如何避免多线程的几点建议。
单线程设计通常涉及由程序组件提供的一些心跳函数,并在循环中调用(称为心跳循环),当调用时,所有组件都有机会做下一步工作并再次放弃控制权。算法家喜欢将组件内部的“循环”视为状态机,以确定下一个应该执行的任务。状态最好作为相应对象的成员数据进行维护。
有很多简单的“死锁解决方案”。但没有一种易于应用且普遍有效的。
当然,最简单的方法是“永远不要有多个线程”。
假设您有一个多线程应用程序,仍然有许多解决方案:
您可以尝试最小化共享状态和同步。两个仅并行运行且从未交互的线程永远不会发生死锁。只有当多个线程尝试访问相同资源时才会发生死锁。为什么他们这样做?是否可以避免这种情况?资源是否可以重组或分割,以便例如一个线程可以写入它,并且其他线程异步传递所需的数据?
也许可以复制资源,使每个线程都有自己的私有副本来处理?
正如其他答案已经提到的那样,如果你尝试获取锁,应该以全局一致的顺序执行。为了简化此过程,您应该尝试确保线程将需要的所有锁都作为单个操作获得。如果一个线程需要获取锁 A、B 和 C,则不应该在不同的时间和地点进行三个 lock()
调用。这会使您感到困惑,无法跟踪线程持有哪些锁,哪些锁尚未获取,然后会混乱。如果您可以一次性获取所有所需的锁,则可以将其分解为单独的函数调用,该函数获得 N 个锁,并按正确的顺序避免死锁。
然后还有更雄心勃勃的方法:像 CSP 这样的技术使得处理线程非常简单,并且即使有数千个并发线程也很容易证明正确性。但它要求您以与您习惯的程序结构非常不同的方式构建程序。
事务性内存是另一个有前途的选择,可能更容易集成到传统程序中。但是生产质量的实现仍然非常罕见。
如果您想要避免死锁的可能性,您必须攻击死锁存在的4个关键条件之一。
死锁的4个条件是: 1. 互斥 - 只有一个线程可以进入临界区。 2. 持有和等待 - 线程在完成任务之前不会释放已获取的资源,即使其他资源不可用。 3. 不可抢占 - 线程没有优先级。 4. 资源循环 - 必须存在一个线程的循环链,该链从其他线程那里等待资源。
最容易攻击的条件是资源循环,通过确保不可能出现循环来实现。