【问题标题】:Fast performing and thread safe observable collection快速执行和线程安全的可观察集合
【发布时间】:2011-12-02 23:21:48
【问题描述】:

ObservableCollections 会针对对其执行的每个操作发出通知。首先它们没有批量添加或删除调用,其次它们不是线程安全的。

这不会让他们变慢吗?我们不能有更快的选择吗?有人说ICollectionView 包裹在ObservableCollection 上很快?这种说法有多真实

【问题讨论】:

  • 试试这个集合,它可以解决这个问题以及其他多线程问题(尽管任何跨线程解决方案都会更慢),这将不可避免地出现其他方法:codeproject.com/Articles/64936/…跨度>
  • 当您说“线程安全”时,您的意思是您需要能够从多个线程更新集合吗?

标签: wpf thread-safety observablecollection icollectionview


【解决方案1】:

ObservableCollection 可以很快,如果它想要的话。 :-)

下面的代码是线程安全、更快的可观察集合的一个很好的示例,您可以根据自己的意愿进一步扩展它。

using System.Collections.Specialized;

public class FastObservableCollection<T> : ObservableCollection<T>
{
    private readonly object locker = new object();

    /// <summary>
    /// This private variable holds the flag to
    /// turn on and off the collection changed notification.
    /// </summary>
    private bool suspendCollectionChangeNotification;

    /// <summary>
    /// Initializes a new instance of the FastObservableCollection class.
    /// </summary>
    public FastObservableCollection()
        : base()
    {
        this.suspendCollectionChangeNotification = false;
    }

    /// <summary>
    /// This event is overriden CollectionChanged event of the observable collection.
    /// </summary>
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    /// <summary>
    /// This method adds the given generic list of items
    /// as a range into current collection by casting them as type T.
    /// It then notifies once after all items are added.
    /// </summary>
    /// <param name="items">The source collection.</param>
    public void AddItems(IList<T> items)
    {
       lock(locker)
       {
          this.SuspendCollectionChangeNotification();
          foreach (var i in items)
          {
             InsertItem(Count, i);
          }
          this.NotifyChanges();
       }
    }

    /// <summary>
    /// Raises collection change event.
    /// </summary>
    public void NotifyChanges()
    {
        this.ResumeCollectionChangeNotification();
        var arg
             = new NotifyCollectionChangedEventArgs
                  (NotifyCollectionChangedAction.Reset);
        this.OnCollectionChanged(arg);
    }

    /// <summary>
    /// This method removes the given generic list of items as a range
    /// into current collection by casting them as type T.
    /// It then notifies once after all items are removed.
    /// </summary>
    /// <param name="items">The source collection.</param>
    public void RemoveItems(IList<T> items)
    {
        lock(locker)
        {
           this.SuspendCollectionChangeNotification();
           foreach (var i in items)
           {
             Remove(i);
           }
           this.NotifyChanges();
        }
    }

    /// <summary>
    /// Resumes collection changed notification.
    /// </summary>
    public void ResumeCollectionChangeNotification()
    {
        this.suspendCollectionChangeNotification = false;
    }

    /// <summary>
    /// Suspends collection changed notification.
    /// </summary>
    public void SuspendCollectionChangeNotification()
    {
        this.suspendCollectionChangeNotification = true;
    }

    /// <summary>
    /// This collection changed event performs thread safe event raising.
    /// </summary>
    /// <param name="e">The event argument.</param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        // Recommended is to avoid reentry 
        // in collection changed event while collection
        // is getting changed on other thread.
        using (BlockReentrancy())
        {
            if (!this.suspendCollectionChangeNotification)
            {
                NotifyCollectionChangedEventHandler eventHandler = 
                      this.CollectionChanged;
                if (eventHandler == null)
                {
                    return;
                }

                // Walk thru invocation list.
                Delegate[] delegates = eventHandler.GetInvocationList();

                foreach
                (NotifyCollectionChangedEventHandler handler in delegates)
                {
                    // If the subscriber is a DispatcherObject and different thread.
                    DispatcherObject dispatcherObject
                         = handler.Target as DispatcherObject;

                    if (dispatcherObject != null
                           && !dispatcherObject.CheckAccess())
                    {
                        // Invoke handler in the target dispatcher's thread... 
                        // asynchronously for better responsiveness.
                        dispatcherObject.Dispatcher.BeginInvoke
                              (DispatcherPriority.DataBind, handler, this, e);
                    }
                    else
                    {
                        // Execute handler as is.
                        handler(this, e);
                    }
                }
            }
        }
    }
}

与任何其他源列表相比,位于ObservableCollection 上方的ICollectionView 也能主动感知更改并执行过滤、分组、排序相对较快。

同样,可观察的集合可能不是更快数据更新的完美解决方案,但它们做得很好。

【讨论】:

  • 定义“更快”。我看不出在添加两个项目时使用 RESET 更改集合会更快,而不是单独添加这两个项目。如果您的收藏中有 1000 个项目,则 UI 将不得不刷新所有 1000 个项目而不是 2 个!我认为在技术上可以编写一个智能的 observable 集合,为您批量更新并使用正确的操作引发集合更改事件,但每次引发 RESET 都是 . . .太可怕了。
  • @Baboon:这完全不正确。将单个项目添加到 ObservableCollection 会引发带有 Action=Add 的单个 CollectionChanged 事件。如果每次集合更改都需要完全刷新,WPF 将非常低效! OP 提供了一个带有 API 和名称的集合,该集合声称速度很快,但在绝大多数用例中它实际上会慢得多。
  • @Kent 、狒狒和格雷戈里,很抱歉没有跟踪这个线程的进度。但是,是的,正如 Gregory 所说,AddItems 实现对于大量项目是实用的。对于几百个,我什至不会费心使用AddItems。但是,当我向集合中添加数以千计的项目时,我获得的性能改进是显着的。你可以自己测试一下。重置 1000 个新添加的项目和 1000 个单独的通知会产生很大的不同。无论如何,WPF 项目容器也会重用虚拟化项目。因此,即使对于 Reset 调用,项目容器也会重用 indi 行。
  • 这是 AddRange 问题的解决方案,但它绝对不是线程安全的。尝试在 ui 上绑定到这个集合,并让一些 UI 和后台线程更新它,你很快就会遇到异常。
  • 这绝对不是线程安全的。即使您在方法周围添加了储物柜,但在父级中实现的方法,即 ObservableCollection 并没有使用它。我不认为你可以通过继承 ObservableCollection 来创建线程安全的集合。
【解决方案2】:

这是我提出的一些解决方案的汇编。收集的想法改变了从第一个答案中获取的调用。

似乎“重置”操作应该与主线程同步,否则 CollectionView 和 CollectionViewSource 会发生奇怪的事情。

我认为这是因为“重置”处理程序尝试立即读取集合内容,并且它们应该已经到位。 如果您执行“重置”异步操作,然后立即添加一些项目,那么新添加的项目可能会被添加两次。

public interface IObservableList<T> : IList<T>, INotifyCollectionChanged
{
}

public class ObservableList<T> : IObservableList<T>
{
    private IList<T> collection = new List<T>();
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    private ReaderWriterLock sync = new ReaderWriterLock();

    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
    {
        if (CollectionChanged == null)
            return;
        foreach (NotifyCollectionChangedEventHandler handler in CollectionChanged.GetInvocationList())
        {
            // If the subscriber is a DispatcherObject and different thread.
            var dispatcherObject = handler.Target as DispatcherObject;

            if (dispatcherObject != null && !dispatcherObject.CheckAccess())
            {
                if ( args.Action == NotifyCollectionChangedAction.Reset )
                    dispatcherObject.Dispatcher.Invoke
                          (DispatcherPriority.DataBind, handler, this, args);
                else
                    // Invoke handler in the target dispatcher's thread... 
                    // asynchronously for better responsiveness.
                    dispatcherObject.Dispatcher.BeginInvoke
                          (DispatcherPriority.DataBind, handler, this, args);
            }
            else
            {
                // Execute handler as is.
                handler(this, args);
            }
        }
    }

    public ObservableList()
    {
    }

    public void Add(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Add(item);
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                      NotifyCollectionChangedAction.Add, item));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public void Clear()
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Clear();
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                        NotifyCollectionChangedAction.Reset));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public bool Contains(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        try
        {
            var result = collection.Contains(item);
            return result;
        }
        finally
        {
            sync.ReleaseReaderLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.CopyTo(array, arrayIndex);
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public int Count
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            try
            {
                return collection.Count;
            }
            finally
            {
                sync.ReleaseReaderLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return collection.IsReadOnly; }
    }

    public bool Remove(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            var index = collection.IndexOf(item);
            if (index == -1)
                return false;
            var result = collection.Remove(item);
            if (result)
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
            return result;
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return collection.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return collection.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        try
        {
            var result = collection.IndexOf(item);
            return result;
        }
        finally
        {
            sync.ReleaseReaderLock();
        }
    }

    public void Insert(int index, T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Insert(index, item);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public void RemoveAt(int index)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            if (collection.Count == 0 || collection.Count <= index)
                return;
            var item = collection[index];
            collection.RemoveAt(index);
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                       NotifyCollectionChangedAction.Remove, item, index));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public T this[int index]
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            try
            {
                var result = collection[index];
                return result;
            }
            finally
            {
                sync.ReleaseReaderLock();
            }
        }
        set
        {
            sync.AcquireWriterLock(Timeout.Infinite);
            try
            {
                if (collection.Count == 0 || collection.Count <= index)
                    return;
                var item = collection[index];
                collection[index] = value;
                OnCollectionChanged(
                        new NotifyCollectionChangedEventArgs(
                           NotifyCollectionChangedAction.Replace, value, item, index));
            }
            finally
            {
                sync.ReleaseWriterLock();
            }
        }

    }
}

【讨论】:

    【解决方案3】:

    我无法添加 cmets,因为我还不够酷,但分享我遇到的这个问题可能值得发布,即使它不是真正的答案。由于 BeginInvoke,使用此 FastObservableCollection 时,我不断收到“索引超出范围”异常。显然,可以在调用处理程序之前撤消通知的更改,因此为了解决此问题,我将以下内容作为从 OnCollectionChanged 方法调用的 BeginInvoke 的第四个参数传递(而不是使用事件参数一):

    dispatcherObject.Dispatcher.BeginInvoke
                              (DispatcherPriority.DataBind, handler, this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    

    而不是这个:

    dispatcherObject.Dispatcher.BeginInvoke
                              (DispatcherPriority.DataBind, handler, this, e);
    

    这解决了我遇到的“索引超出范围”问题。这是更详细的解释/代码snpipet:Where do I get a thread-safe CollectionView?

    【讨论】:

    • 那是因为 FastObservableCollection 不是线程安全的。该链接引用的集合也不是因为它们不提供 TryXXX 方法,因此在尝试访问不再存在的内容时总是会遇到异常等问题,因为检查和操作不是原子的。试试codeproject.com/Articles/64936/…
    【解决方案4】:

    创建同步 Observable 列表的示例:

    newSeries = new XYChart.Series<>();
    ObservableList<XYChart.Data<Number, Number>> listaSerie;
    listaSerie = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<XYChart.Data<Number, Number>>()));
    newSeries.setData(listaSerie);
    

    【讨论】:

    • 请填写您的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多