Як оновити ObservableCollection за допомогою робочого потоку?


83

У мене є ObservableCollection<A> a_collection;колекція, що містить 'n' елементів. Кожен елемент А виглядає так:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

По суті, все це підключено до перегляду списку WPF + b_subcollectionелемента керування переглядом деталей, який відображає вибраний елемент в окремому перегляді списку (двосторонні прив'язки, оновлення властивостей, змінених тощо).

Проблема виявилася для мене, коли я почав впроваджувати різьбу. Повна ідея полягала в тому, щоб a_collectionвикористовувати весь робочий потік, щоб "виконувати роботу", а потім оновлювати їх відповідно, b_subcollectionsі графічний інтерфейс повинен показувати результати в режимі реального часу.

Коли я спробував це, я отримав виняток, сказавши, що лише потік Dispatcher може змінювати ObservableCollection, і робота зупинилася.

Хто-небудь може пояснити проблему і як її обійти?


Спробуйте наступне посилання, яке забезпечує безпечне для потоку рішення, яке працює з будь-якого потоку і до якого можна прив’язати через декілька потоків інтерфейсу користувача: codeproject.com/Articles/64936/…
Ентоні

Відповіді:


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 у фоновому режимі? це також пов'язано з потоком інтерфейсу користувача?
Maciek

1
Так, як розроблено BackgroundWorker, це використання SynchronizationContext.Current для підвищення рівня його завершення та прогресу. Подія DoWork буде виконуватися у фоновому потоці. Ось хороша стаття про створення потоків у WPF, в якій також обговорюється BackgroundWorker msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
Джош,

5
Ця відповідь прекрасна своєю простотою. Дякуємо, що поділилися цим!
Мензурка

@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 Виклик до диспетчера потоків інтерфейсу користувача має ряд недоліків. Найбільшим є те, що ваша колекція не буде оновлюватися, поки потік інтерфейсу фактично не обробить диспетчеризацію, і тоді ви будете працювати на потоці інтерфейсу, що може спричинити проблеми з реагуванням. З іншим методом блокування, ви негайно оновлюєте колекцію і можете продовжувати виконувати обробку у фоновому потоці, незалежно від того, що робить потік інтерфейсу. Потік інтерфейсу буде наздоганяти зміни в наступному циклі візуалізації за необхідності.
Mike Marynowski

2
Я розглядаю синхронізацію колекції в 4.5 вже близько місяця, і не думаю, що частина цієї відповіді є правильною. У відповіді зазначено, що виклик увімкнення повинен відбуватися в потоці інтерфейсу користувача, а зворотний виклик відбувається в потоці інтерфейсу користувача. Здається, нічого з цього немає. Я можу ввімкнути синхронізацію колекції у фоновому потоці і все ще використовувати цей механізм. Крім того, глибокі виклики у фреймворку не роблять жодного маршалінгу (див. 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)));

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);
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.