Wie aktualisiere ich eine ObservableCollection über einen Arbeitsthread?

83

Ich habe eine ObservableCollection<A> a_collection;Die Sammlung enthält 'n' Gegenstände. Jeder Punkt A sieht folgendermaßen aus:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

Grundsätzlich ist alles mit einer WPF-Listenansicht + einem Detailansicht-Steuerelement verbunden, das b_subcollectiondas ausgewählte Element in einer separaten Listenansicht anzeigt (bidirektionale Bindungen, Aktualisierungen bei geänderten Eigenschaften usw.).

Das Problem trat für mich auf, als ich anfing, Threading zu implementieren. Die gesamte Idee war, dass der gesamte a_collectionArbeitsthread verwendet wird, um "Arbeit zu erledigen" und dann die jeweiligen zu aktualisieren b_subcollectionsund die GUI die Ergebnisse in Echtzeit anzeigen zu lassen.

Als ich es versuchte, bekam ich eine Ausnahme, die besagte, dass nur der Dispatcher-Thread eine ObservableCollection ändern kann und die Arbeit zum Stillstand kam.

Kann jemand das Problem erklären und wie man es umgeht?

Maciek
quelle
Versuchen Sie den folgenden Link, der eine thread-sichere Lösung bietet, die von jedem Thread aus funktioniert und über mehrere UI-Threads gebunden werden kann: codeproject.com/Articles/64936/…
Anthony

Antworten:

74

Technisch gesehen besteht das Problem nicht darin, dass Sie die ObservableCollection über einen Hintergrundthread aktualisieren. Das Problem ist, dass die Sammlung in diesem Fall das CollectionChanged-Ereignis für denselben Thread auslöst, der die Änderung verursacht hat. Dies bedeutet, dass Steuerelemente von einem Hintergrundthread aktualisiert werden.

Um eine Sammlung aus einem Hintergrund-Thread zu füllen, während Steuerelemente daran gebunden sind, müssten Sie wahrscheinlich Ihren eigenen Sammlungstyp von Grund auf neu erstellen, um dies zu beheben. Es gibt jedoch eine einfachere Option, die für Sie möglicherweise funktioniert.

Veröffentlichen Sie die Add-Aufrufe im UI-Thread.

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

Diese Methode wird sofort zurückgegeben (bevor das Element tatsächlich zur Sammlung hinzugefügt wird). Anschließend wird das Element im UI-Thread zur Sammlung hinzugefügt, und alle sollten zufrieden sein.

Die Realität ist jedoch, dass diese Lösung aufgrund der gesamten Cross-Thread-Aktivität wahrscheinlich unter starker Last ins Stocken geraten wird. Eine effizientere Lösung würde eine Reihe von Elementen stapeln und sie regelmäßig an den UI-Thread senden, damit Sie nicht für jedes Element über mehrere Threads hinweg aufrufen.

Die BackgroundWorker- Klasse implementiert ein Muster, mit dem Sie den Fortschritt während einer Hintergrundoperation über die ReportProgress- Methode melden können . Der Fortschritt wird im UI-Thread über das ProgressChanged-Ereignis gemeldet. Dies kann eine weitere Option für Sie sein.

Josh
quelle
Was ist mit dem runWorkerAsyncCompleted des BackgroundWorker? Ist das auch an den UI-Thread gebunden?
Maciek
1
Ja, die Art und Weise, wie BackgroundWorker entwickelt wurde, besteht darin, den SynchronizationContext.Current zu verwenden, um die Abschluss- und Fortschrittsereignisse zu erhöhen. Das DoWork-Ereignis wird im Hintergrundthread ausgeführt. Hier ist ein guter Artikel über das Threading in WPF, der auch BackgroundWorker behandelt. Msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
Josh
5
Diese Antwort ist in ihrer Einfachheit schön. Danke, dass du es geteilt hast!
Becher
@Michael In den meisten Fällen sollte der Hintergrund-Thread nicht blockieren und auf die Aktualisierung der Benutzeroberfläche warten. Bei Verwendung von Dispatcher.Invoke besteht die Gefahr eines Dead Locking, wenn die beiden Threads aufeinander warten und die Leistung Ihres Codes bestenfalls erheblich beeinträchtigen. In Ihrem speziellen Fall müssen Sie dies möglicherweise auf diese Weise tun, aber in den allermeisten Situationen ist Ihr letzter Satz einfach nicht korrekt.
Josh
@ Josh Ich habe meine Antwort gelöscht, weil mein Fall etwas Besonderes zu sein scheint. Ich werde in meinem Design weiter schauen und noch einmal darüber nachdenken, was besser gemacht werden könnte.
Michael
125

Neue Option für .NET 4.5

Ab .NET 4.5 gibt es einen integrierten Mechanismus, mit dem der Zugriff auf die Erfassungs- und Versandereignisse automatisch CollectionChangedan den UI-Thread synchronisiert werden kann . Um diese Funktion zu aktivieren, müssen Sie in Ihrem UI-Thread aufrufen .BindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronization macht zwei Dinge:

  1. Erinnert sich an den Thread, von dem aus er aufgerufen wird, und veranlasst die Datenbindungspipeline, CollectionChangedEreignisse in diesem Thread zu marshallen.
  2. Erwirbt eine Sperre für die Sammlung, bis das Marshalled-Ereignis behandelt wurde, sodass die Ereignishandler, auf denen der UI-Thread ausgeführt wird, nicht versuchen, die Sammlung zu lesen, während sie von einem Hintergrund-Thread geändert wird.

Sehr wichtig ist, dass dies nicht alles erledigt : Um einen thread-sicheren Zugriff auf eine inhärent nicht thread-sichere Sammlung zu gewährleisten , müssen Sie mit dem Framework zusammenarbeiten, indem Sie dieselbe Sperre von Ihren Hintergrund-Threads erhalten, wenn die Sammlung geändert werden soll.

Daher sind die für den korrekten Betrieb erforderlichen Schritte:

1. Entscheiden Sie, welche Art von Verriegelung Sie verwenden möchten

Dies bestimmt, welche Überlast von EnableCollectionSynchronizationverwendet werden muss. In den meisten Fällen reicht eine einfache lockAnweisung aus, sodass diese Überlastung die Standardauswahl ist. Wenn Sie jedoch einen ausgefallenen Synchronisationsmechanismus verwenden, werden auch benutzerdefinierte Sperren unterstützt .

2. Erstellen Sie die Sammlung und aktivieren Sie die Synchronisierung

Rufen Sie je nach gewähltem Sperrmechanismus die entsprechende Überlastung des UI-Threads auf . Wenn Sie eine Standardanweisung verwenden lock, müssen Sie das Sperrobjekt als Argument angeben. Wenn Sie eine benutzerdefinierte Synchronisierung verwenden, müssen Sie einen CollectionSynchronizationCallbackDelegaten und ein Kontextobjekt (das sein kann null) bereitstellen . Beim Aufrufen muss dieser Delegat Ihre benutzerdefinierte Sperre erwerben, die Actionübergebene Sperre aufrufen und die Sperre aufheben, bevor er zurückkehrt.

3. Sperren Sie die Sammlung, bevor Sie sie ändern

Sie müssen die Sammlung auch mit demselben Mechanismus sperren, wenn Sie sie selbst ändern möchten. Führen Sie dies mit lock()demselben Sperrobjekt aus, an das EnableCollectionSynchronizationim einfachen Szenario übergeben wurde, oder mit demselben benutzerdefinierten Synchronisierungsmechanismus im benutzerdefinierten Szenario.

Jon
quelle
2
Blockiert dies Sammlungsaktualisierungen, bis der UI-Thread damit fertig ist, sie zu verarbeiten? In Szenarien mit datengebundenen Einweg-Sammlungen unveränderlicher Objekte (ein relativ häufiges Szenario) scheint es möglich zu sein, eine Sammlungsklasse zu haben, die eine "zuletzt angezeigte Version" jedes Objekts sowie eine Änderungswarteschlange enthält und verwenden Sie BeginInvokediese Option, um eine Methode auszuführen, die alle entsprechenden Änderungen im UI-Thread ausführt [höchstens eine BeginInvokezu einem bestimmten Zeitpunkt ausstehende.
Supercat
1
Ich wusste nicht einmal, dass es das gibt! Danke, dass du das geschrieben hast!
Kelly
15
Ein kleines Beispiel würde diese Antwort viel nützlicher machen. Ich denke, es ist wahrscheinlich die richtige Lösung, aber ich habe keine Ahnung, wie ich sie implementieren soll.
RubberDuck
2
@Kohanz Das Aufrufen des UI-Thread-Dispatchers hat eine Reihe von Nachteilen. Das größte Problem ist, dass Ihre Sammlung erst aktualisiert wird, wenn der UI-Thread den Versand tatsächlich verarbeitet. Anschließend wird der UI-Thread ausgeführt, was zu Problemen mit der Reaktionsfähigkeit führen kann. Mit der Sperrmethode hingegen aktualisieren Sie die Sammlung sofort und können die Verarbeitung Ihres Hintergrundthreads fortsetzen, ohne dass der UI-Thread etwas unternimmt. Der UI-Thread holt die Änderungen beim nächsten Renderzyklus nach Bedarf ein.
Mike Marynowski
2
Die Antwort auf diesen Thread über EnableCollectionSynchronization enthält weitere
Matthew S
22

Mit .NET 4.0 können Sie diese Einzeiler verwenden:

.Add

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

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
WhileTrueSleep
quelle
11

Sammlungssynchronisationscode für die Nachwelt. Dies verwendet einen einfachen Sperrmechanismus, um die Sammlungssynchronisierung zu aktivieren. Beachten Sie, dass Sie die Sammlungssynchronisierung im UI-Thread aktivieren müssen.

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
quelle