【问题标题】:Dynamic multi-object locking - Multithreading动态多对象锁定 - 多线程
【发布时间】:2013-12-16 19:21:06
【问题描述】:

锁定不同(动态)数量的对象/键的最佳策略是什么?

考虑这样的场景,当在多个对象上获得锁时(对象数组是动态的,无法预测),线程只能继续执行任务(事务)。在这个例子中,一个 ID 可以代表一个 Object,它需要作为“事务”的一部分进行修改。

示例:

线程:要锁定的对象(作为事务的一部分)

T1:       A   B   C   D

T2:           B       D

T3:       A           D

编辑:改进示例

显然,进行顺序动态锁定会导致所有线程死锁,因为T1 可以获得A 的锁定,而T2 获得B 上的锁定,而T3 获得@987654327 上的锁定@。 T1 等待T2 释放BT2 等待T3 释放DT3 等待T1 释放A

实现这种多对象锁定有哪些可能的选择?

这个问题部分是理论上的,部分是实际的,因为它必须在 C# / .NET 中解决

可能的解决方案:

为了既保持并行性又保持正确的锁定,我想到了以下方案:

两个队列:

  1. 顺序队列(仅由 1 个线程提供服务,因此是顺序的)
  2. 并行队列(由线程池提供服务)

当对 N 个对象的请求到达时,检查每个对象 ID 以及是否为每个 ID 增加锁计数(这可以是 Dictionary<int, int> - <Id, Lock Count>)。

如果所有 ID 都被“锁定”(注意实际上没有发生锁定),即第一次请求,则将请求放入并行队列 ELSE 将请求放入顺序队列中

这种混合方法允许按顺序处理“竞争”请求,并允许并行处理非竞争请求。

【问题讨论】:

  • 为什么 T2 等待 T1 释放 A? T2 没有锁定 A
  • @dcastro 因为 T1 抢了 A 然后 B,同时 T2 想要 B,所以它会等待 T1 完成 C 和 D 才释放 B。
  • 是的,同意这个例子可以改进——关键是有问题的场景涉及两个线程持有一些键,另一个线程也想要这些。
  • 考虑 T1:A B -- T2:B A,其中每个人都抓住了他们的第一把锁,而不是他们的第二把。这将陷入僵局。您需要确保您的代码不会发生这种情况,这并不总是那么容易。
  • @DavidHaney 您的序列是有效的,但在给出的示例中,B 被 T2 抓取。

标签: c# .net multithreading


【解决方案1】:

在锁定多个对象时防止死锁的方法是有一个获取锁的规范顺序。在您的示例中,让我们创建必须按字母顺序获取锁的方案。假设T1 获得AT2 获得BT3 在获得A 之前不应尝试获得D。然后B 可以获取D 并完成。然后T1 可以获取B,随后T3 可以获取A

但是,不能在代码库的任何地方违反此方案。如果你已经获取了锁ABD,你就不能返回去获取C

反证法:

假设这个方案可能会死锁。这意味着所有线程都在等待锁。如果必须按顺序获取所有锁,则意味着所有锁都可以映射到整数1 ... N。一个获取的锁L 必须是序列中获取的最高锁。但是,该线程不能被阻塞,因为没有线程可以拥有高于L 的锁。因此不可能所有线程都被阻塞。

【讨论】:

  • 我想这是唯一的解决方案,如果使用锁定(而不是重新设计)。所以接受这个和zmbq的答案,因为它们是相似的。
  • 显然你不能在这里接受两个答案,所以不得不选择这个,因为它有“定理证明”。
【解决方案2】:

我同意@Reed 的观点,如果可以重新设计代码以避免所有这些锁定,那就去做吧。

重新设计它的一个非常简单的方法是,因为你有这么多锁,所以完全失去并发性。按顺序运行所有内容。您可能会发现它的运行速度同样快,因为无论如何所有锁定都在阻止并发。

如果由于某种原因您不能这样做,确保不会出现死锁的一种方法是始终以相同的顺序获取锁。在您的示例中,如果一个任务需要同时锁定 A 和 D,总是在 D 之前锁定 A。在所有其他任务中也要这样做。

这样死锁是不可能的。当任务 1 拥有锁 A 并想要锁 B,而任务 2 拥有锁 B 并想要锁 A 时,就会发生死锁。如果您总是先锁定 A,然后再锁定 B,那么任务 2 就不可能拥有锁 B,然后又想要锁 A。

【讨论】:

  • 我想到了一种混合顺序/并行方法(请参阅编辑) - 它与仅顺序相比如何公平?
  • 如果不先尝试,就无法预测。我会先尝试顺序方法,它可能足够快,并且是迄今为止最容易实现的。
【解决方案3】:

一般来说,您应该尽一切可能避免尝试以这种方式锁定多个对象。当您开始可选地锁定多个资源时,避免死锁变得非常困难。

相反,重新思考设计并提出其他策略几乎总是一种更好的方法。例如,使用不可变类型可以避免在许多情况下完全需要锁定。并发集合对于避免锁定也非常有益,因为您可以将数据处理与生产(生产者/消费者通过BlockingCollection<T> 等)分开。

【讨论】:

  • 这是事实。您不应该同时锁定超级粒度和顺序。选择一个或另一个。顺序广泛的锁总是以相同的顺序获得,或者不是顺序获得的粒度锁。
  • 想知道如何通过改变设计来保持“事务”功能 - 多个客户端(并行请求)请求多个对象的操作,并且只能执行一旦所有对象都被锁定(事务)。使请求处理顺序否定并行性。
  • @user2983917 如果您可以流水线化操作,那可能会有所帮助。查看 TPL 数据流了解更多详情。
  • @user2983917 根本问题始于为什么需要事务功能?
  • 如果其中一个对象的操作失败,所有对象都将恢复到请求前状态,从而实现事务功能。
猜你喜欢
  • 1970-01-01
  • 2020-10-31
  • 2017-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多