快速执行和线程安全的可观察集合。

26

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

这难道不会使它们变慢吗?难道我们没有更快的替代方案吗?一些人说,包装在ObservableCollection周围的ICollectionView是快速的?这种说法有多真实呢?


试试这个集合,它不仅可以解决这个问题,还能处理其他多线程问题(尽管任何跨线程的解决方案都会变慢),并且在其他方法中不可避免地出现:http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection - Anthony
当你说“线程安全”时,你的意思是需要能够从多个线程更新集合吗? - Slugart
4个回答

76

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);
                    }
                }
            }
        }
    }
}

除此之外,ICollectionView 位于 ObservableCollection 之上,可以主动感知变化并执行过滤、分组、排序等操作,相对于其他源列表来说速度更快。

再次强调,可观察集合可能不是更快数据更新的完美解决方案,但它们能够胜任其工作。


13
定义“更快”。我不明白如果添加两个项时使用RESET触发集合更改如何可能比仅分别添加这两个项更快。如果集合中有1000个项,则UI必须刷新所有1000个项,而不是2个!我认为在技术上编写一个智能的可观察集合来批处理更新并以正确的操作引发集合更改事件是可能的,但每次都引发RESET太糟糕了。 - Kent Boogaart
2
@Kent、Baboon和Gregory,很抱歉没有跟进这个帖子的进展。但是,正如Gregory所说,AddItems实现对于大量项目是实用的。对于几百个项目,我甚至不会费心使用AddItems。但是,当我向集合中添加数千个项目时,我获得的性能提升是显着的。您可以自行测试。1000个新添加的项目和1000个单独的通知的重置会产生很大的差异。此外,WPF项容器在任何情况下都会重用虚拟化的项。因此,即使是Reset调用,项容器也会重用indi行。 - WPF-it
9
这是对AddRange问题的解决方案,但它绝对不是线程安全的。如果尝试在UI上绑定到此集合,并让UI和后台线程同时更新它,将很快遇到异常。 - Anthony
1
@Anthony,谢谢你为我记录下来。我现在已经添加了locker对象。请查看编辑。再次感谢。 - WPF-it
4
这绝对不是线程安全的。即使你在方法周围添加了锁,但父类中实现的方法,即ObservableCollection没有使用它。我认为您无法通过子类化ObservableCollection来创建线程安全的集合。 - Rashack
显示剩余15条评论

5
这里是我总结的一些解决方案,灵感来自第一个答案提到的收集修改调用。另外似乎“重置”操作应该与主线程同步,否则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();
            }
        }

    }
}

2

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

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

不是这样:

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

我遇到了“索引超出范围”的问题,这个解决方案解决了我的问题。以下是更详细的说明/代码片段:我从哪里获取线程安全的CollectionView?


这是因为FastObservableCollection不是线程安全的。链接中提到的集合也不是,因为它们没有提供TryXXX方法,因此当尝试访问不存在的内容时,您将始终遇到异常问题,因为检查和操作不是原子的。请尝试使用http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection。 - Anthony

-1
一个创建同步的可观察列表的示例:
newSeries = new XYChart.Series<>();
ObservableList<XYChart.Data<Number, Number>> listaSerie;
listaSerie = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<XYChart.Data<Number, Number>>()));
newSeries.setData(listaSerie);

请完成您的回答。 - Sulthan Allaudeen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接