可观察对象的同步机制

时间:2013-12-03 19:54:18

标签: c# .net multithreading thread-synchronization

让我们假设我们必须同步对共享资源的读/写访问。多个线程将在读取和写入时访问该资源(大多数时间用于读取,有时用于写入)。让我们假设每次写操作总是会触发读操作(对象是可观察的)。

对于这个例子,我会想象一个这样的类(原谅语法和风格,它仅用于说明目的):

class Container {
    public ObservableCollection<Operand> Operands;
    public ObservableCollection<Result> Results;
}

为了这个目的,我很想使用ReadWriterLockSlim而且我把它放在Container级别(想象对象不是那么简单,一个读/写操作可能涉及多个对象):

public ReadWriterLockSlim Lock;

OperandResult的实现对此示例没有意义。 现在让我们想象一些观察Operands的代码并生成一个放入Results的结果:

void AddNewOperand(Operand operand) {
    try {
        _container.Lock.EnterWriteLock();
        _container.Operands.Add(operand);
    }
    finally {
        _container.ExitReadLock();
    }
}

我们的hypotetical观察者会做类似的事情,但是要使用一个新元素,它会锁定EnterReadLock()来获取操作数,然后EnterWriteLock()来添加结果(让我省略代码)。这会因为递归而产生异常,但如果我设置LockRecursionPolicy.SupportsRecursion,那么我只需将代码打开到死锁(来自MSDN):

  

默认情况下,使用LockRecursionPolicy.NoRecursion标志创建ReaderWriterLockSlim的新实例,并且不允许递归。建议对所有新开发使用此默认策略,因为递归会引入不必要的复杂情况,会使您的代码更容易出现死锁

为了清楚起见,我重复了相关部分:

递归[...]使您的代码更容易出现死锁。

如果我对LockRecursionPolicy.SupportsRecursion没有错,如果来自同一个帖子,我问一个,比如读锁,然后某人其他人要求写锁定然后我就死了-lock然后MSDN说的是有道理的。此外,递归也会以可衡量的方式降低性能(如果我使用的是ReadWriterLockSlim而不是ReadWriterLockMonitor,那么这不是我想要的。

问题(S)

最后我的问题是(请注意我不是在寻找关于一般同步机制的讨论,我知道这个生产者/观察者/观察者场景有什么问题):

  • 在这种情况下有什么好处?为了避免ReadWriterLockSlim支持Monitor(即使在现实世界中,代码读取的内容远远超过写入)?
  • 放弃这种粗略的同步?这甚至可以产生更好的性能,但它会使代码更复杂(当然不是在这个例子中,而是在现实世界中)。
  • 我应该只是通知(来自观察到的收集)异步吗?
  • 我看不到的其他东西?

我知道没有最佳同步机制,所以我们使用的工具必须是正确的,但我想知道是否有一些最佳实践或我只是忽略线程和观察者之间非常重要的事情(假设使用Microsoft Reactive Extensions但问题是一般的,不与该框架相关联。)

可能的解决方案?

我会尝试将事件(某种程度上)推迟:

第一个解决方案
每次更改都不会触发任何CollectionChanged事件,它会保留在队列中。当提供者(推送数据的对象)完成时,它将手动强制刷新队列(按顺序引发每个事件)。这可以在另一个线程中完成,甚至可以在调用者线程中完成(但在锁定之外)。

它可能有效,但它会使所有内容变得不那么“自动”(每个更改通知必须由生产者本身手动触发,编写更多代码,更多错误)。

第二个解决方案
另一种解决方案可能是为我们的 lock 提供对可观察集合的引用。如果我将ReadWriterLockSlim包装在自定义对象中(用于将其隐藏在易于使用的IDisposable对象中),我可以添加ManualResetEvent以通知所有锁已经以这种方式释放本身可能会引发事件(再次出现在同一个线程或另一个线程中)。

第三种解决方案
另一个想法可能是使事件异步。如果事件处理程序需要锁定,那么它将被停止以等待它的时间范围。为此,我担心可能使用的大线程数量(特别是如果来自线程池)。

老实说,我不知道这些是否适用于现实世界的应用程序(个人 - 从用户的角度来看 - 我更喜欢第二个但它意味着所有内容的自定义集合,它使集合意识到线程,我会避免它,如果可能的话)。我不想让代码比必要的更复杂。

3 个答案:

答案 0 :(得分:6)

这听起来像是多线程的泡菜。在这种事件链模式中使用递归是非常具有挑战性的,同时仍然避免死锁。您可能需要考虑完全围绕问题进行设计。

例如,您可以添加操作数与引发事件异步:

private readonly BlockingCollection<Operand> _additions
    = new BlockingCollection<Operand>();

public void AddNewOperand(Operand operand)
{
    _additions.Add(operand);
}

然后在后台线程中进行实际添加:

private void ProcessAdditions()
{
    foreach(var operand in _additions.GetConsumingEnumerable())
    {
        _container.Lock.EnterWriteLock();
        _container.Operands.Add(operand);
        _container.Lock.ExitWriteLock();
    }
}

public void Initialize()
{
    var pump = new Thread(ProcessAdditions)
    {
        Name = "Operand Additions Pump"
    };
    pump.Start();
}

这种分离牺牲了一些一致性 - 在add方法实际上不知道添加实际发生的时间后运行的代码可能会对您的代码造成问题。如果是这样,可以重写这个以订阅观察并使用Task在添加完成时发出信号:

public Task AddNewOperandAsync(Operand operand)
{
    var tcs = new TaskCompletionSource<byte>();

    // Compose an event handler for the completion of this task
    NotifyCollectionChangedEventHandler onChanged = null;
    onChanged = (sender, e) =>
    {
        // Is this the event for the operand we have added?
        if (e.NewItems.Contains(operand))
        {
            // Complete the task.
            tcs.SetCompleted(0);

            // Remove the event-handler.
            _container.Operands.CollectionChanged -= onChanged;
        }
    }

    // Hook in the handler.
    _container.Operands.CollectionChanged += onChanged;

    // Perform the addition.
    _additions.Add(operand);

    // Return the task to be awaited.
    return tcs.Task;
}

事件处理程序逻辑在后台线程上引发添加消息,因此不可能阻塞前台线程。如果您等待窗口的消息泵上的添加,同步上下文足够智能,也可以在消息泵线程上安排继续。

无论您是否沿着Task路线走下去,这种策略意味着您可以安全地从可观察事件中添加更多操作数,而无需重新输入任何锁定。

答案 1 :(得分:1)

我不确定这是否完全相同,但在处理相对少量的数据(2k-3k条目)时,我使用以下代码来促进对绑定到UI的集合的跨线程读/写访问。此代码最初找到here

public class BaseObservableCollection<T> : ObservableCollection<T>
{
  // Constructors
  public BaseObservableCollection() : base() { }
  public BaseObservableCollection(IEnumerable<T> items) : base(items) { }
  public BaseObservableCollection(List<T> items) : base(items) { }

  // Evnet
  public override event NotifyCollectionChangedEventHandler CollectionChanged;

  // Event Handler
  protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
  {
    // Be nice - use BlockReentrancy like MSDN said
    using (BlockReentrancy())
    {
      if (CollectionChanged != null)
      {
        // Walk thru invocation list
        foreach (NotifyCollectionChangedEventHandler handler in CollectionChanged.GetInvocationList())
        {
          DispatcherObject dispatcherObject = handler.Target as DispatcherObject;

          // If the subscriber is a DispatcherObject and different thread
          if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
          {
            // Invoke handler in the target dispatcher's thread
            dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
          }
          else
          {
            // Execute handler as is
            handler(this, e);
          }
        }
      }
    }
  }
}

我还使用下面的代码(继承自上面的代码)来支持在集合中的项目引发CollectionChanged时引发PropertyChanged事件。

public class BaseViewableCollection<T> : BaseObservableCollection<T>
  where T : INotifyPropertyChanged
{
  // Constructors
  public BaseViewableCollection() : base() { }
  public BaseViewableCollection(IEnumerable<T> items) : base(items) { }
  public BaseViewableCollection(List<T> items) : base(items) { }

  // Event Handlers
  private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
  {
    var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender);
    base.OnCollectionChanged(arg);
  }

  protected override void ClearItems()
  {
    foreach (T item in Items) { if (item != null) { item.PropertyChanged -= ItemPropertyChanged; } }
    base.ClearItems();
  }

  protected override void InsertItem(int index, T item)
  {
    if (item != null) { item.PropertyChanged += ItemPropertyChanged; }
    base.InsertItem(index, item);
  }

  protected override void RemoveItem(int index)
  {
    if (Items[index] != null) { Items[index].PropertyChanged -= ItemPropertyChanged; }
    base.RemoveItem(index);
  }

  protected override void SetItem(int index, T item)
  {
    if (item != null) { item.PropertyChanged += ItemPropertyChanged; }
    base.SetItem(index, item);
  }
}

答案 2 :(得分:0)

跨线程集合同步

将ListBox绑定放到 ObservableCollection ,当数据发生更改时,更新ListBox,因为实现了INotifyCollectionChanged。 缺陷dell'ObservableCollection是数据只能由创建它的线程更改。

SynchronizedCollection 没有多线程的问题,但是没有更新ListBox,因为它没有实现INotifyCollectionChanged,即使你实现了INotifyCollectionChanged,也只能调用CollectionChanged(this,e)从创建它的线程..所以它不起作用。

<强>结论

- 如果您想要一个自动更新的单线程列表,请使用 ObservableCollection

- 如果您想要一个未自动更新但多线程的列表,请使用 SynchronizedCollection

- 如果您想要两者,请以这种方式使用Framework 4.5 ,BindingOperations.EnableCollectionSynchronization和ObservableCollection():

/ / Creates the lock object somewhere
private static object _lock = new object () ;
...
/ / Enable the cross acces to this collection elsewhere
BindingOperations.EnableCollectionSynchronization ( _persons , _lock )

完整示例 http://10rem.net/blog/2012/01/20/wpf-45-cross-thread-collection-synchronization-redux