【发布时间】:2013-12-20 01:19:21
【问题描述】:
假设我们必须同步对共享资源的读/写访问。多个线程将以读取和写入的方式访问该资源(大部分时间用于读取,有时用于写入)。我们还假设每次写入都会触发一次读取操作(对象是可观察的)。
对于这个例子,我会想象一个这样的类(请原谅语法和风格,这只是为了说明目的):
class Container {
public ObservableCollection<Operand> Operands;
public ObservableCollection<Result> Results;
}
我很想为此使用ReadWriterLockSlim,而且我会把它放在Container 级别(想象对象不是那么简单,一个读/写操作可能涉及多个对象):
public ReadWriterLockSlim Lock;
Operand 和 Result 的实现对于本示例没有任何意义。
现在让我们想象一些代码观察到Operands,并将产生一个结果放入Results:
void AddNewOperand(Operand operand) {
try {
_container.Lock.EnterWriteLock();
_container.Operands.Add(operand);
}
finally {
_container.ExitReadLock();
}
}
我们假设的观察者会做类似的事情,但要消耗一个新元素,它会用EnterReadLock() 锁定来获取操作数,然后用EnterWriteLock() 来添加结果(让我省略代码)。由于递归,这将产生一个异常,但如果我设置LockRecursionPolicy.SupportsRecursion,那么我只会将我的代码打开死锁(来自MSDN):
默认情况下,ReaderWriterLockSlim 的新实例是使用 LockRecursionPolicy.NoRecursion 标志创建的,并且不允许递归。建议对所有新开发使用此默认策略,因为递归会引入不必要的复杂性并使您的代码更容易出现死锁。
为了清楚起见,我重复相关部分:
递归 [...] 使您的代码更容易出现死锁。
如果我对LockRecursionPolicy.SupportsRecursion 没有错,如果我从同一个线程问一个,比如说,读锁,然后 有人 否则要求一个写锁,那么我就会死锁那么MSDN所说的有道理。此外,递归也会以可衡量的方式降低性能(如果我使用ReadWriterLockSlim 而不是ReadWriterLock 或Monitor,这不是我想要的)。
问题
最后我的问题是(请注意,我不是在寻找关于一般同步机制的讨论,我会知道这个生产者/可观察者/观察者场景有什么问题):
- 在这种情况下有什么更好的方法?避免
ReadWriterLockSlim支持Monitor(即使在现实世界中,代码读取将远多于写入)? - 放弃如此粗略的同步?这甚至可能会产生更好的性能,但它会使代码更加复杂(当然不是在这个例子中,而是在现实世界中)。
- 我应该让通知(来自观察到的集合)异步吗?
- 还有什么我看不到的?
我知道没有最佳同步机制,所以我们使用的工具必须适合我们的情况,但我想知道是否有一些最佳实践,或者我只是忽略线程和观察者之间非常重要的事情(想象一下使用Microsoft Reactive Extensions,但问题是一般性的,与该框架无关)。
可能的解决方案?
我会尝试(以某种方式)推迟事件:
第一种解决方案
每个更改都不会触发任何CollectionChanged 事件,它保存在队列中。当提供者(推送数据的对象)完成后,它将手动强制刷新队列(按顺序引发每个事件)。这可以在另一个线程中完成,甚至可以在调用者线程中完成(但在锁之外)。
它可能有效,但它会让一切变得不那么“自动”(每个更改通知都必须由生产者自己手动触发,要编写更多代码,周围有更多错误)。
第二个解决方案
另一种解决方案可能是为我们的 lock 提供对 observable 集合的引用。如果我将ReadWriterLockSlim 包装在自定义对象中(有助于将其隐藏在易于使用的IDisposable 对象中),我可以添加ManualResetEvent 以通知所有锁都已以这种方式释放,集合本身可能会引发事件(再次在同一个线程或另一个线程中)。
第三种解决方案
另一个想法可能是让事件异步。如果事件处理程序需要一个锁,那么它将停止等待它的时间框架。为此,我担心可能会使用大量线程(特别是如果来自线程池)。
老实说,我不知道这些是否适用于现实世界的应用程序(就个人而言 - 从用户的角度来看 - 我更喜欢第二个,但这意味着对所有内容进行自定义收集,它使收集意识到线程,我会避免如果可能的话)。我不想让代码变得过于复杂。
【问题讨论】:
-
我只想提一下,我发现
EnterReadLock和Add的组合非常可怕。该代码打算只读取,但它也写入集合。您确定不想在该特定时间点使用EnterWriteLock? -
@Caramiriel 你是对的,我固定了例子!
-
如果你让你的方法更粗略一些,例如将 Operands 和 Result 设为只读属性并添加 AddOperand 和 AddResult 方法,您将能够将 Lock 设为私有并更好地控制发生的事情。还是我完全没有抓住重点?
-
@flup 你说得对。我的问题是它会使一切变得更加复杂,并且模型会意识到线程(如果可能的话,我会避免这种情况,因为当它在单个线程中使用时会影响性能设想)。此外,模型本身当然比我的例子复杂得多。可能是一个线程安全层使用您建议的方法构建的模型?
-
你不能使用 ConcurrentQueue 和/或 BlockingCollection 吗? ObservableCollections 用于您需要以某种方式处理整个集合的情况,但如果您只是在添加新操作数时添加新结果,那听起来像是基于流的操作。或者,或者,使用成对的操作数结果集合怎么样?同样,您也许可以使用某种现有的 Concurrent-collection 类,并且不会出现所有这些问题。
标签: c# .net multithreading thread-synchronization