Быстро производительная и потокобезопасная наблюдаемая коллекция

ObservableCollections вызывают уведомления для каждого выполняемого над ними действия. Во-первых, у них нет вызовов массового добавления или удаления, во-вторых, они не являются потокобезопасными.

Разве это не делает их медленнее? У нас нет более быстрой альтернативы? Некоторые говорят, что ICollectionView обернуть ObservableCollection быстро? Насколько правдиво это утверждение.


person Community    schedule 07.10.2011    source источник
comment
Попробуйте эту коллекцию, которая решает эту проблему, а также другие многопоточные проблемы (хотя любое многопоточное решение будет медленнее), которые неизбежно возникнут с другими подходами: codeproject.com/Articles/64936/   -  person Anthony    schedule 17.04.2014
comment
Когда вы говорите потокобезопасность, вы имеете в виду, что вам нужно иметь возможность обновлять коллекцию из нескольких потоков?   -  person Slugart    schedule 20.08.2014


Ответы (4)


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, активно осведомлен об изменениях и выполняет фильтрацию, группировку и сортировку относительно быстро по сравнению с любым другим исходным списком.

Опять же, наблюдаемые коллекции могут быть не идеальным решением для более быстрого обновления данных, но они хорошо справляются со своей задачей.

person WPF-it    schedule 07.10.2011
comment
+1 за нестандартную реализацию. Но разве Microsoft где-нибудь так говорит о паре ICollectionView и ObservableCollection? - person ; 07.10.2011
comment
У меня нет BlockReentrancy () или DispatcherObject в моем проекте Silverlight, мне не хватает ссылки или это WPF? - person Adriaan Davel; 02.02.2012
comment
Другой вопрос, если это потокобезопасно И быстрее, почему Microsoft не делает этого для стандартной ObservableCollection? Есть ли у этого «цена», означающая, что это не лучшая реализация «по умолчанию»? - person Adriaan Davel; 02.02.2012
comment
Определите быстрее. Я не понимаю, как можно быстрее поднять коллекцию, измененную с помощью RESET, при добавлении двух элементов, а не просто добавлять эти два элемента по отдельности. Если в вашей коллекции 1000 элементов, пользовательский интерфейс придется обновлять для всех 1000 элементов, а не для 2! Я думаю, что технически возможно написать интеллектуальную наблюдаемую коллекцию, которая собирает вместе обновления для вас и вызывает события изменения коллекции с правильными действиями, но каждый раз поднимает RESET. . . ужасный. - person Kent Boogaart; 09.07.2012
comment
@KentBoogaart, к сожалению, WPF слушает CollectionChanged только при сбросе. - person Louis Kottmann; 18.10.2012
comment
@KentBoogaart Хорошей идеей было бы установить пороговое значение, до которого будет уведомляться коллекция для каждого добавленного элемента, и вызвать сброс при превышении порога. - person ghord; 14.11.2012
comment
@Baboon: это совершенно неверно. Добавление одного элемента в ObservableCollection вызывает одно событие CollectionChanged с Action=Add. WPF был бы ужасно неэффективным, если бы требовал полного обновления при каждом изменении коллекции! OP представляет коллекцию с API и именем, которое претендует на то, чтобы быть быстрым, хотя на самом деле он будет намного медленнее в подавляющем числе случаев использования. - person Kent Boogaart; 14.11.2012
comment
@Gregory: Думаю, ты немного упускаешь суть. Событие CollectionChanged (и класс NotifyCollectionChangedEventArgs) уже обслуживает одно событие, чтобы уведомить о множественных добавлениях / удалениях / чем угодно, если эти изменения являются последовательными в коллекции. - person Kent Boogaart; 14.11.2012
comment
@KentBoogaart Я реализовал ObservableLinkedList, и без сброса пользовательский интерфейс не обновляется. Так что нет, это не совсем неверно, это опыт. - person Louis Kottmann; 14.11.2012
comment
@Baboon: извините, но моя точка зрения остается неизменной. WPF отлично справляется с единственным событием ObservableCollection с действием Add. Вы, должно быть, допустили ошибку где-то в своей реализации. Вы можете очень легко доказать это, создав OC ‹string›, прикрепив обработчик событий и добавив элемент. Вы получаете одно событие с действием Добавить. - person Kent Boogaart; 14.11.2012
comment
@KentBoogaart Я говорю о наследовании от ObservableCollection или создании над ним. Прямое использование работает (но не вы напрямую инициируете событие). - person Louis Kottmann; 14.11.2012
comment
@ Бабуин: бессмысленно, извините. WPF не волнует, кто инициирует мероприятие. Возможно, вам стоит опубликовать свою реализацию в отдельном вопросе, и я могу вмешаться. - person Kent Boogaart; 14.11.2012
comment
@KentBoogart Конечно, ObservableCollection может запускать события для одного добавленного элемента. Проблема, которую решает этот код (и почему он вызывается быстро), заключается в добавлении большого количества элементов в коллекцию. Если вы добавляете много элементов по одному, вы заметите огромное падение производительности из-за большого количества запущенных событий NotifyCollectionChanged. Вот почему лучше на время подавить событие добавления и сбросить коллекцию, когда нужно добавить достаточное количество элементов. - person ghord; 15.11.2012
comment
@Kent, Baboon & Gregory, извините за то, что не отслеживаете прогресс в этой теме. Но да, как сказал Грегори, реализация AddItems практична для большого количества элементов. Несколько сотен я даже не стал бы использовать AddItems. Но улучшение производительности, которое я получаю, когда добавляю в коллекцию несколько тысяч элементов, является значительным. Вы можете проверить это сами. Сброс для 1000 новых добавленных элементов и 1000 отдельных уведомлений имеет большое значение. Кроме того, контейнер элементов WPF в любом случае повторно использует виртуализированные элементы. Таким образом, даже для вызова Reset контейнер элементов повторно использует отдельные строки. - person WPF-it; 15.11.2012
comment
DispatcherObject и prioty не могут использоваться моей общей библиотекой. Что я могу с этим поделать? - person Tugrul Emre Atalay; 03.12.2013
comment
Когда вы говорите, что моя общая библиотека не может использовать, что вы имеете в виду? - person WPF-it; 03.12.2013
comment
Это решение проблемы AddRange, но определенно небезопасно для потоков. Попробуйте выполнить привязку к этой коллекции в пользовательском интерфейсе и попросите обновить ее как пользовательский интерфейс, так и фоновые потоки, и вы быстро столкнетесь с исключениями. - person Anthony; 15.04.2014
comment
@Anthony, спасибо, что отметили это для меня. Я добавил объект шкафчика. Смотрите редактирование. Еще раз спасибо. - person WPF-it; 28.08.2014
comment
Это определенно небезопасно для потоков. Несмотря на то, что вы добавили шкафчик вокруг своих методов, методы, реализованные в родительском элементе, то есть ObservableCollection, его не используют. Я не думаю, что вы действительно можете создать потокобезопасную коллекцию, создав подкласс ObservableCollection. - person Rashack; 19.05.2015
comment
Что если изменить IList на IEnumerable? Я думаю, что производительность была бы лучше, например, при использовании Linq нам не нужно вызывать ToList () для компиляции. - person Lei Yang; 29.06.2016

Вот подборка некоторых решений, которые я сделал. Идея сбора изменила вызов, взятый из первого ответа.

Также кажется, что операция «Reset» должна быть синхронной с основным потоком, иначе странные вещи случатся с CollectionView и CollectionViewSource.

Я думаю, это потому, что при «Reset» обработчик пытается сразу прочитать содержимое коллекции, и они уже должны быть на месте. Если вы выполните «Сброс» асинхронно, а затем сразу же добавите некоторые элементы, также асинхронно, новые добавленные элементы могут быть добавлены дважды.

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

    }
}
person norekhov    schedule 03.04.2014

Я не могу добавлять комментарии, потому что я еще недостаточно крут, но, вероятно, стоит опубликовать эту проблему, с которой я столкнулся, даже если это не совсем ответ. Я продолжал получать исключение «Индекс вне допустимого диапазона», используя этот FastObservableCollection, из-за BeginInvoke. Очевидно, уведомления об изменениях могут быть отменены до вызова обработчика, поэтому, чтобы исправить это, я передал следующее в качестве четвертого параметра для BeginInvoke, вызываемого из метода OnCollectionChanged (в отличие от использования аргументов события один):

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

Вместо этого:

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

Это устранило проблему «Индекс вне допустимого диапазона», с которой я столкнулся. Вот более подробное объяснение / фрагмент кода: Где я могу получить поток- безопасный CollectionView?

person jsirr13    schedule 10.06.2013
comment
Это потому, что FastObservableCollection не является потокобезопасным. Коллекции, на которые ссылается эта ссылка, не являются также потому, что они не предоставляют методы TryXXX, и из-за этого у вас всегда будут проблемы, такие как исключения, при попытке доступа к чему-то, чего больше нет, потому что проверка и операции не являются атомарными. Попробуйте codeproject.com/Articles/64936/ - person Anthony; 15.04.2014

Пример создания синхронизированного списка 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);
person Raul    schedule 26.03.2014
comment
Пожалуйста, дополните свой ответ. - person Sulthan Allaudeen; 26.03.2014