Как обновить ObservableCollection через рабочий поток?

83

У меня есть ObservableCollection<A> a_collection;коллекция содержит n элементов. Каждый элемент A выглядит так:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

По сути, все это связано со списком WPF + b_subcollectionэлементом управления подробным представлением, который показывает выбранный элемент в отдельном списке (двусторонние привязки, обновления при изменении свойств и т. Д.).

Проблема обнаружилась у меня, когда я начал реализовывать потоки. Вся идея заключалась в том, чтобы весь a_collectionрабочий поток использовался для «работы», а затем обновлял их b_subcollectionsи отображал результаты в реальном времени.

Когда я попробовал это, у меня было исключение, в котором говорилось, что только поток Dispatcher может изменять ObservableCollection, и работа остановилась.

Может ли кто-нибудь объяснить проблему и как ее обойти?

Maciek
источник
Попробуйте следующую ссылку, которая предоставляет поточно-ориентированное решение, которое работает из любого потока и может быть привязано через несколько потоков пользовательского интерфейса: codeproject.com/Articles/64936/…
Anthony

Ответы:

74

Технически проблема не в том, что вы обновляете ObservableCollection из фонового потока. Проблема в том, что когда вы это делаете, коллекция вызывает событие CollectionChanged в том же потоке, который вызвал изменение, что означает, что элементы управления обновляются из фонового потока.

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

Разместите вызовы Add в потоке пользовательского интерфейса.

public static void AddOnUI<T>(this ICollection<T> collection, T item) {
    Action<T> addMethod = collection.Add;
    Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}

...

b_subcollection.AddOnUI(new B());

Этот метод вернется немедленно (до того, как элемент будет фактически добавлен в коллекцию), а затем в потоке пользовательского интерфейса элемент будет добавлен в коллекцию, и все будут счастливы.

Реальность, однако, такова, что это решение, скорее всего, зависнет при большой нагрузке из-за всей межпоточной активности. Более эффективное решение могло бы объединить кучу элементов и периодически публиковать их в потоке пользовательского интерфейса, чтобы вы не вызывали потоки для каждого элемента.

Класс BackgroundWorker реализует шаблон, который позволяет вам сообщать о ходе выполнения через его метод ReportProgress во время фоновой операции. О ходе выполнения сообщается в потоке пользовательского интерфейса через событие ProgressChanged. Это может быть еще один вариант для вас.

Джош
источник
как насчет runWorkerAsyncCompleted BackgroundWorker? это тоже связано с потоком пользовательского интерфейса?
Maciek
1
Да, BackgroundWorker спроектирован таким образом, чтобы использовать SynchronizationContext.Current для создания событий завершения и выполнения. Событие DoWork будет запущено в фоновом потоке. Вот хорошая статья о многопоточности в WPF, в которой также обсуждается BackgroundWorker msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
Джош,
5
Этот ответ прекрасен своей простотой. Спасибо, что поделились этим!
Beaker
@Michael В большинстве случаев фоновый поток не должен блокироваться и ждать обновления пользовательского интерфейса. Использование Dispatcher.Invoke сопряжено с риском мертвой блокировки, если два потока будут ждать друг друга, и в лучшем случае значительно снизит производительность вашего кода. В вашем конкретном случае вам может потребоваться сделать это таким образом, но для подавляющего большинства ситуаций ваше последнее предложение просто неверно.
Джош
@Josh Я удалил свой ответ, потому что мой случай кажется особенным. Я буду смотреть дальше в своем дизайне и еще раз подумаю, что можно было бы сделать лучше.
Майкл
125

Новая опция для .NET 4.5

Начиная с .NET 4.5 существует встроенный механизм для автоматической синхронизации доступа к сбору и отправки CollectionChangedсобытий в поток пользовательского интерфейса. Чтобы включить эту функцию, вам необходимо позвонить из потока пользовательского интерфейса .BindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronization делает две вещи:

  1. Запоминает поток, из которого он вызывается, и заставляет конвейер привязки данных маршалировать CollectionChangedсобытия в этом потоке.
  2. Получает блокировку коллекции до тех пор, пока маршалированное событие не будет обработано, так что обработчики событий, выполняющие поток пользовательского интерфейса, не будут пытаться прочитать коллекцию, пока она изменяется из фонового потока.

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

Следовательно, шаги, необходимые для правильной работы:

1. Решите, какой тип блокировки вы будете использовать.

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

2. Создайте коллекцию и включите синхронизацию.

В зависимости от выбранного механизма блокировки вызовите соответствующую перегрузку в потоке пользовательского интерфейса . При использовании стандартного lockоператора вам необходимо предоставить объект блокировки в качестве аргумента. При использовании настраиваемой синхронизации вам необходимо предоставить CollectionSynchronizationCallbackделегата и объект контекста (что может быть null). При вызове этот делегат должен получить вашу настраиваемую блокировку, вызвать Actionпереданную ему и снять блокировку перед возвратом.

3. Сотрудничайте, заблокировав коллекцию перед ее изменением.

Вы также должны заблокировать коллекцию, используя тот же механизм, когда собираетесь изменить ее самостоятельно; сделайте это с lock()тем же объектом блокировки, переданным EnableCollectionSynchronizationв простом сценарии, или с тем же настраиваемым механизмом синхронизации в настраиваемом сценарии.

Джон
источник
2
Вызывает ли это блокировку обновлений коллекции до тех пор, пока поток пользовательского интерфейса не приступит к их обработке? В сценариях, включающих односторонние коллекции неизменяемых объектов с привязкой к данным (относительно распространенный сценарий), казалось бы, можно было бы иметь класс коллекции, который будет хранить «последнюю отображаемую версию» каждого объекта, а также очередь изменений. , и используйте BeginInvokeдля запуска метода, который будет выполнять все соответствующие изменения в потоке пользовательского интерфейса [ BeginInvokeв любой момент времени не более одного будет ожидающим выполнения.
supercat
16
Небольшой пример сделает этот ответ гораздо более полезным. Я думаю, что это, наверное, правильное решение, но я не знаю, как его реализовать.
RubberDuck
3
@Kohanz Вызов диспетчера потоков пользовательского интерфейса имеет ряд недостатков. Самая большая из них заключается в том, что ваша коллекция не будет обновляться до тех пор, пока поток пользовательского интерфейса не обработает отправку, а затем вы будете работать в потоке пользовательского интерфейса, что может вызвать проблемы с быстродействием. С другой стороны, с помощью метода блокировки вы сразу же обновляете коллекцию и можете продолжать обработку в фоновом потоке, независимо от того, что поток пользовательского интерфейса что-либо делает. Поток пользовательского интерфейса при необходимости догонит изменения в следующем цикле рендеринга.
Майк Мариновски
2
Я уже около месяца смотрю на синхронизацию коллекций в 4.5 и не думаю, что некоторые из этих ответов верны. В ответе указано, что вызов enable должен происходить в потоке пользовательского интерфейса, а обратный вызов - в потоке пользовательского интерфейса. Ни то, ни другое, похоже, не так. Я могу включить синхронизацию коллекции в фоновом потоке и по-прежнему использовать этот механизм. Кроме того, глубокие вызовы в структуре не выполняют никакой сортировки (см. ViewManager.AccessCollection. Referenceource.microsoft.com/#PresentationFramework/src/… )
Reginald Blue
2
Ответ на этот поток об EnableCollectionSynchronization дает больше информации: stackoverflow.com/a/16511740/2887274
Matthew S,
22

В .NET 4.0 вы можете использовать эти однострочники:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
WhileTrueSleep
источник
11

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

public class MainVm
{
    private ObservableCollection<MiniVm> _collectionOfObjects;
    private readonly object _collectionOfObjectsSync = new object();

    public MainVm()
    {

        _collectionOfObjects = new ObservableCollection<MiniVm>();
        // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
        Application.Current.Dispatcher.BeginInvoke(new Action(() => 
        { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
    }

    /// <summary>
    /// A different thread can access the collection through this method
    /// </summary>
    /// <param name="newMiniVm">The new mini vm to add to observable collection</param>
    private void AddMiniVm(MiniVm newMiniVm)
    {
        lock (_collectionOfObjectsSync)
        {
            _collectionOfObjects.Insert(0, newMiniVm);
        }
    }
}
LadderLogic
источник