针对这个例子,我将想象一个类,如下所示(原谅语法和风格,仅供说明用途):
class Container {
public ObservableCollection<Operand> Operands;
public ObservableCollection<Result> Results;
}
我打算使用
ReadWriterLockSlim
来实现这个目的,而且我会将其放置在容器级别上(想象一下对象不是那么简单,一个读写操作可能涉及多个对象):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):
我重复相关部分以明确: 递归[...]使您的代码更容易出现死锁。 如果我没有理解错,如果我在同一个线程中请求读锁,然后“某人”请求写锁,那么我将会遇到死锁,这是因为默认情况下,使用LockRecursionPolicy.NoRecursion标志创建ReaderWriterLockSlim的新实例,不允许递归。建议所有新开发都使用此默认策略,因为递归引入了不必要的复杂性,并使您的代码更容易出现死锁。
LockRecursionPolicy.SupportsRecursion
。此外,递归也会以可测量的方式降低性能(如果我使用ReadWriterLockSlim
而不是ReadWriterLock
或Monitor
,这不是我想要的)。问题:
最后我的问题是(请注意,我不是在寻找关于一般同步机制的讨论,我想知道在这种生产者/可观察者/观察者场景下有什么问题):
- 在这种情况下,什么更好?放弃
ReadWriterLockSlim
,转而使用Monitor
(即使在实际代码中读取远多于写入)?
- 放弃如此粗略的同步?这甚至可能会带来更好的性能,但它会使代码变得更加复杂(当然不是在这个例子中,而是在实际应用中)。
- 我只需使通知(从观察到的集合)异步吗?
- 还有其他我看不到的东西吗?
我知道不存在一个最好的同步机制,所以我们使用的工具必须是适合我们情况的,但我想知道在线程和观察者之间是否有一些最佳实践或者我是否忽略了一些非常重要的事情(假设使用Microsoft Reactive Extensions,但问题是普遍的,不局限于该框架)。
可能的解决方案?
我会尝试使事件(以某种方式)延迟:
第一种解决方案
每次更改都不会触发任何CollectionChanged
事件,而是将其保留在队列中。当提供程序(推送数据的对象)完成后,它将手动强制刷新队列(按顺序引发每个事件)。这可以在另一个线程中完成,甚至可以在调用线程中完成(但在锁定外部)。
这可能有效,但它会使一切变得不那么“自动化”(每个更改通知必须由生产者自己手动触发,需要编写更多的代码,到处都有更多的错误)。
第二个解决方案
另一个解决方案可能是向可观察集合提供对我们的lock的引用。如果我在自定义对象中包装ReadWriterLockSlim
(有助于将其隐藏在易于使用的IDisposable
对象中),我可以添加一个ManualResetEvent
来通知所有锁已被释放,这样集合本身就可以在同一线程或另一线程中引发事件。
第三个解决方案
另一个想法可能只是使事件异步化。如果事件处理程序需要锁定,则会停止等待其时间框架。对此,我担心可能使用大量线程(特别是如果来自线程池)。
老实说,我不知道这些解决方案是否适用于实际应用程序(个人而言-从用户的角度来看-我更喜欢第二种方法,但它意味着为每个东西创建自定义集合,并且它使集合了解线程,如果可能,我会避免它)。我不想使代码比必要的复杂。
EnterReadLock
和Add
组合相当可怕。代码的意图是仅进行读操作,但实际上它也会将数据写入集合。您确定在该特定点不想使用EnterWriteLock
吗? - Caramiriel