锁定不同(动态)数量的对象/键的最佳策略是什么?
考虑这样的场景,其中线程只能继续执行任务(事务),当在多个对象上获得锁定时(对象数组是动态的并且无法预测)。在此示例中,ID可以表示对象,需要将其作为“事务”的一部分进行修改。
例:
线程:要锁定的对象(作为事务的一部分)
T1: A B C D
T2: B D
T3: A D
编辑:改进示例
显然,执行顺序动态锁定,可能会导致所有线程死锁,因为T1
可以锁定A
,而T2
锁定B
,T3
抓住D
锁定。 T1
等待T2
释放B
,T2
等待T3
释放D
,T3
等待T1
释放A
。
有哪些可能的选项来实现这种多对象锁定?
问题部分是理论上的,部分是实际的,因为它必须在C#/ .NET中解决
可能解决方案
为了保持并行性并保持正确的锁定,我想到了以下方案:
两个队列:
当请求到达N个对象时,检查每个对象Id以及每个ID的增量锁定计数(这可以是Dictionary<int, int> - <Id, Lock Count>
)。
如果所有ID都被“锁定”(注意没有发生实际锁定),即第一次请求,则将请求放入并行队列ELSE将请求放入顺序队列
这种混合方法允许顺序处理“竞争”请求,而不是竞争 - 并行处理。
锁定多个对象时防止死锁的方法是获取锁定的规范顺序。在您的示例中,让我们创建必须按字母顺序获取锁的方案。说T1
获得A
和T2
获得B
。 T3
不应该尝试获得D
,直到它获得A
。然后B
可以获得D
并完成。然后T1
可以获得B
,随后T3
可以获得A
。
但是,在代码库中的任何地方都不能违反此方案。如果你已经获得了锁A
,B
和D
,你就不能回去获得C
。
矛盾证明:
假设使用此方案可能会死锁。这意味着所有线程都在等待锁定。如果必须按顺序获取所有锁,则意味着所有锁都可以映射到整数1 ... N
。一个获得的锁L
必须是序列中获得的最高锁。但是,该线程无法阻止,因为没有线程可以拥有高于L
的锁。因此,不可能阻止所有线程。
我同意@Reed,如果有可能重新设计代码以避免所有这些锁 - 做到这一点。
一个非常简单的方法来重新设计它,因为你有这么多的锁,就是完全失去并发性。顺序运行一切。您可能会发现它的工作速度同样快,因为所有锁定都会阻止并发性。
如果由于某种原因无法做到这一点,那么确保不出现死锁的一种方法是始终以相同的顺序获取锁。在您的示例中,如果任务同时需要锁A和D,则始终在D之前锁定A.在所有其他任务中也执行此操作。
这种方式不可能陷入僵局。当任务1具有锁定A并且想要锁定B并且任务2具有锁定B并且想要锁定A时发生死锁。如果在锁定B之前总是锁定A,则任务2将无法锁定B然后想要锁定A.
通常,您应该尽一切可能避免尝试以这种方式锁定多个对象。当您开始可选地锁定多个资源时,很难避免死锁。
相反,重新思考设计几乎总是更好的方法,并提出其他策略。例如,使用不可变类型可以避免在许多情况下完全锁定的需要。并发集合对于避免锁定也是非常有益的,因为您可以从生产中分离出数据的处理(生产者/消费者通过BlockingCollection<T>
等)。