Laden...

Threadsicherer Zugriff auf ObservableCollection und LINQ

Erstellt von Master15 vor 11 Jahren Letzter Beitrag vor 11 Jahren 10.712 Views
M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 11 Jahren
Threadsicherer Zugriff auf ObservableCollection und LINQ

Hallo C#-Profis,

zunächst wünsche ich hiermit ein gesundes und erfolgreiches neues Jahr 2013.

Ich habe schon länger ein paar Verständnisprobleme mit Threads, Tasks, ObservableCollections (auch in Verbindung mit WPF Bindings), LINQ und wollte deshalb hier im Forum mal bei euch nachfragen.

Ich habe eine Software, die viel asynchron mit der Hardware (Geräten) kommuniziert (z.B. RS232, USB, Ethernet, CAN,...).
Ich verwendet u.a. Threads, BackgroundWorker und mittlerweile am liebsten Tasks. Die Kommunikation selber ist kein Problem, da das meist nach dem Schema Producer-Consumer abläuft.
Ich hatte dafür früher die SyncQueue<T> von herbivore genutzt. Aktuell verwende ich in .NET 4 die BlockingCollection<T>.

Es kommt jetzt jedoch noch ein anderer Bereich hinzu, den ich hier als Beispiel nur vereinfacht darstelle:
Bei der Kommunikation mit den Geräten werden bestimmte Teilnehmer gemeldet und jeder Teilnehmer kann bestimmte Features aufweisen, die er unterstützt.
Kurz: Ein Gerät hat eine Liste aus Teilnehmer. Ein Teilnehmer hat eine Liste aus Features.

Während das Gerät verbunden ist, können jederzeit Teilnehmer oder sogar deren Features wegfallen oder neu hinzukommen.
Solche Änderungen müssen in der Software andere Einheiten mitbekommen und auch der Benutzer, der sich vielleicht gerade die Features eines Teilnehmers ansieht. Deshalb brauche ich dafür ObservableCollections die threadsicher sind.
Vor ca. 2 Jahren hatte ich mit SynchronizedObservableCollection, SafeObservableCollection, ThreadSafeObservableCollection Tests gemacht und letztlich mir einen Verschnitt daraus zusammengebastelt, wobei alle Listenoperationen mittels Dispatcher.Invoke an den UI-Thread weitergereicht werden, sodass auch das NotifyCollectionChanged-Event keine Probleme bei Bindings und WPF macht. Zudem wird intern ein syncRoot-Object gelockt. Auf dies kann man von außen (Property) zugreifen, z.B. wenn man durch die Liste iterieren will (foreach etc.).
Bisher funktionierte das glücklicherweise im Dauerbetrieb (ca. 8h). Neulich kamen aber noch ein paar Tasks dazu und ich müsste dann lange auf Fehlersuche gehen, da es immer mal wieder zu Deadlocks kam.

Der Grund so nebenbei: Ein Task sollte ein Feature durch ein anderes ersetzen und hat zunächst die Liste gelockt, mit Contains nach dem Feature gesucht, es mit Remove gelöscht und eine anderes mit Add hinzugefügt. Da die Listeoperationen jedoch intern mit Dispatcher.Invoke verarbeitet werden, versuchte der UI-Thread ebenfalls die Liste zu locken. Task und UI-Thread haben sich gegenseitig blockiert. Das asynchrone Dispatcher.BeginInvoke führte leider zu anderen Problemen.

Mir wäre es lieber, wenn die Listenoperationen direkt vom jeweiligen Thread/Task ausgeführt werden und nicht über den Dispatcher laufen.
Ich will eventuell auf .NET 4.5 umsteigen und mache deshalb gerade Tests mit der ObservableCollection<T> und BindingOperations.EnableCollectionSynchronization.

Habe mir jetzt mal eine total einfache TestObservableCollection<T> erstellt:

public class TestObservableCollection<T> : ObservableCollection<T>
{
	private Object syncRoot;

	public TestObservableCollection() : base()
	{
		syncRoot = ((ICollection)this).SyncRoot;
		BindingOperations.EnableCollectionSynchronization(this, syncRoot);
	}

	public Object SyncRoot
	{
		get { return syncRoot; }
	}
}

In WPF habe ich eine ListView und DataGrid (CanUserAddRows=True) verwendet und daran die Listen gebunden (zudem lassen sich mehrere Fenster öffnen). Es laufen mehrere Tasks, die Listenoperationen durchführen bzw. durch die Liste iterieren, wobei natürlich das SyncRoot-Objekt gelockt wird.
Bisher lief das reibungslos, aber ich wollte trotzdem mal nachfragen, ob das so Sinn macht, bevor ich das in eine Anwendung einbaue?

Es gibts dann noch so eine Sache mit LINQ. Ich habe LINQ wirklich schätzen gelernt und will das eigentlich nicht mehr missen, aber mir ist das mit der Threadsicherheit nicht klar.
Pseudocode:

public void MachWas(Gerät gerät)
{
	var featureNames = from teilnehmer in gerät.Teilnehmer
			           from feature in teilnehmer.Features
			           where feature.Id < 10 && feature.Status == FeatureStatus.On
			           select feature.Name;
			   
	foreach(String featureName in featureNames)
		//mach was...
}

Wie ich das mit zwei foreach-Schleifen threadsicher machen könnte, ist mir klar. Aber wie funktioniert das in LINQ? Wo baut man dort die lock-Anweisungen ein? Leider spuckt das WWW dazu nichts aus oder ich verwende die falschen Suchbegriffe.

Ich würde mich über eure Tipps freuen!

Viele Grüße
Thomas

16.807 Beiträge seit 2008
vor 11 Jahren

Aber wie funktioniert das in LINQ? Wo baut man dort die lock-Anweisungen ein?

Du erreichst mit Deiner Implementierung die Thread-Sicherheit auf den Typ** T**
Hat dieser Typ eine Property mit einer Collection und Du benötigst hier ebenfalls eine Thread-Sicherheit, so musst Du diese dort ebenfalls implementieren.

Normalerweise sollte sich darum aber die Business-Logik /DAL-Schicht kümmern und nicht die Oberfläche.

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo Master15,

Bisher lief das reibungslos, aber ich wollte trotzdem mal nachfragen, ob das so Sinn macht, bevor ich das in eine Anwendung einbaue?

man muss man zwischen Threadsicherheit und der Vermeidung von unzulässigen threadübergreifenden Vorgängen unterscheiden. Das erste bedeutet, dass man aus mehreren Threads gleichzeitig auf gemeinsame Daten zugreifen darf, ohne dass dadurch Probleme entstehen. Das kann man z.B. durch lock o.ä. erreichen. Das zweite bedeutet, dass alle Zugriffe aus einem bestimmten Thread erfolgen müssen, damit es keine Probleme gibt, unabhängig davon, ob die Zugriffe gleichzeitig erfolgen würden oder nicht. Das kann man z.B. durch Dispatcher.Invoke erreichen.

So wie ich es verstehe, musst du beides sicherstellen.

Die Frage ist nun, was durch BindingOperations.EnableCollectionSynchronization sichergestellt wird. Und da werde aus der Doku nicht schlau. Bei BindingOperations.EnableCollectionSynchronization-Methode steht "Enables a collection to be accessed across multiple threads.". In CollectionSynchronizationCallback-Delegat, dem Delegaten, der von einer der Überladung von EnableCollectionSynchronization benutzt wird steht "Represent the method that synchronizes a collection for cross-thread access." Den ersten Satz kann man vielleicht noch als Threadsicherheit lesen, aber der zweite Satz klingt mir mehr danach, dass es um das Vermeidung von unzulässigen threadübergreifenden Vorgängen geht.

Letztlich würde ich vermuten, dass es bei BindingOperations.EnableCollectionSynchronization nur um das Vermeiden von unzulässigen threadübergreifenden Vorgängen geht, aber keine Threadsicherheit hergestellt wird. Das bedeutet - wenn ich dein Szenario richtig verstanden habe - dass du nur die halbe Miete hast.

Fügst du, um die Threadsicherheit zu erreichen, noch locks hinzu bzw. verwendest eine threadsichere Collection, bekommst du ohne Änderung der Abläufe vermutlich wieder Deadlocks.

Da ich persönlich EnableCollectionSynchronization nicht durchschaue, würde ich zurück zu expliziten Dispatcher.Invoke gehen. Und auch eine echte threadsichere Collection verwenden. Und dann eben die Zugriffe so ändern, dass kein Deadlock mehr auftreten kann. Du hast ja schon erkannt, warum Deadlocks entstehen. Da ist der Schritt, die schädlichen Zugriffe zu unterlassen, nicht mehr weit.

herbivore

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 11 Jahren

Guten Abend,

ich muss gestehen, dass ich die Antwort von Abt nicht so recht verstanden habe. Ich verwende in meinen meisten Anwendungen diverse Entwurfsmuster. Bezogen auf WPF setzte ich eine Art MVC ein, da ich mich mit MVVM bisher nicht recht anfreunden konnte. Business-Logik /DAL-Schicht zähle ich hier mit zur Model-Schicht.

Du erreichst mit Deiner Implementierung die Thread-Sicherheit auf den Typ T
Hat dieser Typ eine Property mit einer Collection und Du benötigst hier ebenfalls eine Thread-Sicherheit, so musst Du diese dort ebenfalls implementieren.

Ist mir nicht ganz klar, wie das gemeint ist. Man könnte natürlich in einer Property (teilnehmer.Features) für Thread-Sicherheit sorgen, indem man nur von dort auf eine interne Liste zugreift, diese lockt und nach außen nur eine Kopie (toList(), toArray(),...) zurückgibt. Damit dürfte das sogar mit LINQ threadsicher sein. Damit bekommt man Änderungen (CollectionChanged-Event) aber dann an anderen Stellen nicht mehr mit.

man muss man zwischen Threadsicherheit und der Vermeidung von unzulässigen threadübergreifenden Vorgängen unterscheiden. Das erste bedeutet, dass man aus mehreren Threads gleichzeitig auf gemeinsame Daten zugreifen darf, ohne dass dadurch Probleme entstehen. Das kann man z.B. durch lock o.ä. erreichen. Das zweite bedeutet, dass alle Zugriffe aus einem bestimmten Thread erfolgen müssen, damit es keine Probleme gibt, unabhängig davon, ob die Zugriffe gleichzeitig erfolgen würden oder nicht. Das kann man z.B. durch Dispatcher.Invoke erreichen.

So wie ich es verstehe, musst du beides sicherstellen.

Danke für die Erklärungen, wieder was gelernt. 👍
Ich denke, dass es in meinem Fall somit hauptsächlich threadübergreifende Vorgänge sind.

Ich habe in der Anwendung diverse Listen und da können zahlreiche Threads drauf zugreifen. Bisher war es oft so, dass Listenoperationen (Add, Remove,...) meist vom Benutzer (UI-Thread) ausgegangen sind und die Threads nur "gelesen" haben.
Diese Anforderung ändern sich jedoch etwas und besonders bei der Kommunikation mit der Hardware kann es vorkommen, dass Threads bzw. Events (z.B. SerialPort.DataReceived) in den Listen Änderungen vornehmen. Deshalb ist mir das mit den Deadlocks auch jetzt erst nach einigen Erweiterungen aufgefallen.

Hinzu kommt noch, dass bei Listenänderungen (CollectionChanged-Event) bestimmte Aktionen in der Model-Schicht von anderen Threads ausführt werden sollen. Deshalb dachte ich, dass eine ObservableCollection<T> da relativ gut passt. Manchmal kommt es halt vor, dass ich solch eine Liste auch mal in der View darstellen will. Besonders schön bei WPF sind natürlich die Bindings z.B. direkt an ItemsSource. Leider kommt WPF jedoch nicht damit klar, wenn das CollectionChanged-Event nicht vom UI-Thread gefeuert wird. Nur deshalb hatte ich damals den Kompromiss mit dem Dispatcher gemacht und dachte nun bei .NET 4.5, dass bei BindingOperations.EnableCollectionSynchronization diese Probleme der Vergangenheit angehören. Jedoch steige ich noch nicht so recht durch, was da genau passiert. Ich wollte mir nicht die Arbeit machen und den .NET Code studieren bzw. eigene Datenstrukturen überlegen. Für ein Privatprojekt ist mir das einfach zu viel Aufwand.

Eine echte threadsichere Liste (observable) kenne ich nicht. Die in .NET 4 eingeführten Thread-safe Collections sind zwar oftmals hilfreich, aber eher als Puffer zu gebrauchen (FIFO, LIFO).

Ich werde eventuell doch mal mit meiner o.g. TestObservableCollection<T> einen Dauertest durchführen. Wenn das ca. 8h ohne Absturz läuft, dann wäre ich schon zufrieden.

Gruß
Thomas

16.807 Beiträge seit 2008
vor 11 Jahren

Bezogen auf WPF setzte ich eine Art MVC ein, da ich mich mit MVVM bisher nicht recht anfreunden konnte. Business-Logik /DAL-Schicht zähle ich hier mit zur Model-Schicht.

Das ist aber falsch. Die Logik steckt bei MVC im Controller und nicht in den Modellen.
File:MVC-Process.png
Man muss aber Modelle für die Datenbank (Entities) und Modelle für die View (ViewModels) unterscheiden.

In der Modell schicht _sollte _keinerlei Intelligenz stecken, weshalb hier sich dann sowas wie

Hinzu kommt noch, dass bei Listenänderungen (CollectionChanged-Event) bestimmte Aktionen in der Model-Schicht von anderen Threads ausführt werden sollen. gar nicht erst ergibt.

2.891 Beiträge seit 2004
vor 11 Jahren

Das ist aber falsch. Die Logik steckt bei MVC im Controller und nicht in den Modellen.

Ui, mit solchen absoluten Aussagen wäre ich aber vorsichtig. Alle Allaussagen sind falsch. 😉
Denn der Controller soll eigentlich recht wenig Logik enthalten und nur die View-Aktionen entsprechend auf das Model abbilden. Die konkrete Geschäftslogik steckt in den Modellen - da hat der Controller nichts mit zu tun.

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo Master15,

Ich werde eventuell doch mal mit meiner o.g. TestObservableCollection<T> einen Dauertest durchführen. Wenn das ca. 8h ohne Absturz läuft, dann wäre ich schon zufrieden.

gerade bei der Synchronisation von nebenläufigen Vorgängen und wegen mögliche Race Coditions erhält man nur eine sehr begrenzte Aussage. Auf einem anderen Rechner oder auch nach einen Betriebsystem-Update kann es ganz anders aussehen. Wo die Race Coditions vorher möglicherweise alle in die eine Richtung ausgegangen sind gehen sie dann möglicherweise öfter in die andere Richtung aus. Wo es vorher 8 Stunden ohne Probleme ging, knallt es nachher vielleicht alle fünf Minuten. Bei Synchronisierung sollte man wissen, was man tut. Einfach etwas auf Verdacht zu programmieren und nachher zu testen, ist keine gute Option. Siehe z.B. SyncQueue <T> - Eine praktische Job-Queue einen Fall, der sich durch einen einfachen Dauertest kaum finden lässt.

herbivore

3.430 Beiträge seit 2007
vor 11 Jahren

Hallo,

ich kann Herbivore nur zustimmen.
Vor kurzen hatte ich ein ähnliches Problem. Das Programm ist über 2 Monate ohne Probleme gelaufen.
Aber aus irgendwelchen misteriösen Gründen funktionierte es nicht mehr.
Beim Logik zum Auslesen der Daten von einer Seriellen Schnittstelle führe es plötzlich immer wieder zu Problemen mit thread-übergreifenden Vorgängen.

Ich habe immer noch keine Ahnung wieso das vorher über einen so langen Zeitraum nie keine Probleme gab. Aber von einen Tag auf den anderen kam es alle 1-2 Minuten zu Fehlern.
Vielleicht war es ein Update, vielleicht sonst was. Keine Ahnung.

Jedenfalls ist es immer wichtig dass man sich genau überlegt was man macht. Und sich nicht nur auf Dauertests verlässt.

Grüße
Michael

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 11 Jahren

Hallo,

zunächst Danke für die Antworten.

Das ist aber falsch. Die Logik steckt bei MVC im Controller und nicht in den Modellen.

Ich kann nur jedem Entwickler raten, die eigentliche Logik der Software nicht in die Controller-Schicht zu packen.

gerade bei der Synchronisation von nebenläufigen Vorgängen und wegen mögliche Race Coditions erhält man nur eine sehr begrenzte Aussage. Auf einem anderen Rechner oder auch nach einen Betriebsystem-Update kann es ganz anders aussehen. Wo die Race Coditions vorher möglicherweise alle in die eine Richtung ausgegangen sind gehen sie dann möglicherweise öfter in die andere Richtung aus. Wo es vorher 8 Stunden ohne Probleme ging, knallt es nachher vielleicht alle fünf Minuten. Bei Synchronisierung sollte man wissen, was man tut. Einfach etwas auf Verdacht zu programmieren und nachher zu testen, ist keine gute Option. Siehe z.B. SyncQueue <T> - Eine praktische Job-Queue einen Fall, der sich durch einen einfachen Dauertest kaum finden lässt.

Jedenfalls ist es immer wichtig dass man sich genau überlegt was man macht. Und sich nicht nur auf Dauertests verlässt.

Ich gebe euch beiden vollkommen Recht. Mir wäre auch eine anständige Lösung am liebsten, wenn ich die kennen würde.
Ich sehe aktuell nur die hier genannten Ansätze über den Dispatcher, was sicherlich auch noch Ärger macht oder aber den genannte Kompromiss in .NET 4.5, den ich auch noch nicht komplett durchschaue.

Ich muss zu meiner Schande gestehen, dass mir meine ältere zusammengebastelte Lösung über den Dispatcher auch schon Kopfschmerzen bereitet hat, da ich nicht so wirklich weiß, wie der Dispatcher intern arbeitet.

Mal so rein theoretisch angenommen:
Wir haben eine Art ObservableCollection, deren Operationen über den Dispatcher laufen und die Collection zudem ein SyncRoot-Objekt (public property) zur Verfügung stellt, um auch von außerhalb ein lock(list.SyncRoot) durchführen zu können.
Es gibt ein MainWindow (WPF), mit einem Button. Dort kann der Benutzer weitere Fenster öffnen, das jeweils eine ListView beinhaltet. Die Collection wird an die ItemsSource-Eigenschaft übergeben (bzw. Binding).

Über den internen Ablauf der Steuerelemente habe ich bisher leider nicht so viele nützliche Hinweise finden können. Sicherlich muss durch die Collection iteriert werden, um die Items anzuzeigen. Das CollectionChanged-Event wird auch registriert, um spätere Änderungen mitzubekommen. Doch was passiert, wenn genau während dieses Vorgangs ein Thread beispielsweise Add aufruft. Es wird dann durch den Dispatcher an den UI-Thread weitergereicht.
Was macht dann der UI-Thread? Unterbricht er eventuell sogar die Iteration durch die Collection, was dann zu einer InvalidOperationException (Collection was modified) führt, oder registriert er das Event womöglich zu früh bzw. zu spät?
Sorry für diese Frage, aber ich konnte das in meinen Büchern bzw. Tutorials nirgends nachlesen und oftmals ist ja sogar noch eine ListCollectionView beteiligt.

Viele Grüße
Thomas
@Michael (michlG): Der Solar Inspector sieht interessant aus 👍

16.807 Beiträge seit 2008
vor 11 Jahren

Um meine Aussage zu rechtfertigen, und weshalb mein Post vielleicht deutlicher sein sollte:
Ich bin kein Freund von Fat Models und aufgrund der ganzen aktuellem ORM-Schiene und der dahingehenden Probleme mit Methoden innerhalb von Modellen (Stichwort Lazy Loading).
Natürlich gehört Business-Logik ausgelagert und nicht DIREKT in den Controller aber er wird von Controller aufgerufen (zum Beispiel Repositories) und nicht in den Modellen (und dann sogar noch innerhalb der View-Generierung bei direkter Weitergabe ⚠).
Optimal wäre natürlich ein Ein-Zeiler im Controller, der die gegebenen Parameter direkt an die BL weitergibt und daraus auf ViewModel formt (siehe etliche ASP MVC Beispiele von mir in diesem Forum).

Ich frag mich aber: wenn Du Dir doch so unsicher in Deine Implementierung bist, wieso schaust Du Dir nicht eine der vielen vielen Implementierungen diesbezüglich an, die bereits existieren?

Thread Safe Improvement for ObservableCollection
Thread-Safe & Dispatcher-Safe Observable Collection for WPF
ThreadSafeObservableCollection of (T)
WP7Contrib: Thread safe ObservableCollection

Der Zugriff auf die Elemente selbst ist aber in den Implementierungen nicht Thread-Sicher, da direkter Zugriff auf den Enumerator. Thread-Safe ist hier quasi nur Add und Delete etc.
Du könntest Dir aber noch eine Methode basteln, die quasi eine Kopie der aktuellen Liste liefert (einfaches ToList()) - dennoch ist hier aber auch der Zugriff auf das Objekt selbst sicher zu stellen.

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo Master15,

Über den internen Ablauf der Steuerelemente habe ich bisher leider nicht so viele nützliche Hinweise finden können. [...] Was macht dann der UI-Thread? Unterbricht er eventuell sogar die Iteration durch die Collection. [...] Sorry für diese Frage, aber ich konnte das in meinen Büchern bzw. Tutorials nirgends nachlesen.

mag sein, dass in den Büchern nicht direkt steht, ob die laufende Verarbeitung unterbrochen wird, aber in den Büchern und im Netz steht genug, um sich die Abläufe selber zu erklären. Deshalb gehe ich hier nicht näher darauf ein, sondern verweise dich auf [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke). Da das GUI single-thtreaded arbeitet und demzufolge die Nachrichten sequentiell verarbeitet und Dispatcher.Invoke einfach eine Nachricht schickt, ergibt sich die Antwort in einem Schritt. Deshalb bitte keine weiteren Fragen zu diesem Aspekt.

herbivore

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 11 Jahren

Guten Tag,

mag sein, dass in den Büchern nicht direkt steht, ob die laufende Verarbeitung unterbrochen wird, aber in den Büchern und im Netz steht genug, um sich die Abläufe selber zu erklären. Deshalb gehe ich hier nicht näher darauf ein, sondern verweise dich auf [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke). Da das GUI single-thtreaded arbeitet und demzufolge die Nachrichten sequentiell verarbeitet und Dispatcher.Invoke einfach eine Nachricht schickt, ergibt sich die Antwort in einem Schritt. Deshalb bitte keine weiteren Fragen zu diesem Aspekt.

Leider kann man sich nicht alle Aspekte selber erklären. Wenn das so wäre, bräuchte man sicherlich auch kein Forum. Vielleicht bin ich aber auch einfach nicht so schlau wie ihr.
Diese sequentielle Verarbeitung hatte ich mir bisher immer schon so gedacht, aber nach den Effekten die ich zuletzt erlebt habe, hat sich mein Verständnis in Luft aufgelöst.
Obwohl alles über Dispatcher.Invoke geht, konnte ich beim Öffnen von Fenstern mehrfach erkennen, das z.B. 3 Elemente in der ListView angegezeigt werden, obwohl in der Collection definitiv nur 2 vorkommen (ein Objekt wurde in der ListView mehrfach eingefügt, lässt sich auch an der doppelten blauen Selektierung erkennen).
Aber egal, es muss dann andere Gründe haben, denen ich nachgehen werde. Habe heute mit einem .NET-Entwickler über den UI-Thread und Dispatcher geredet. Daran sollte es ja eigentlich nicht liegen. Muss mal suchen, wo da der Wurm drin ist.

Ich frag mich aber: wenn Du Dir doch so unsicher in Deine Implementierung bist, wieso schaust Du Dir nicht eine der vielen vielen Implementierungen diesbezüglich an, die bereits existieren?

Hatte ich eigentlich schon im ersten Beitrag geschrieben, dass ich vor ca. 2 Jahren schon ein paar Implementierungen getestet hatte. Die meisten hatten das Problem, dass threadübergreifende Vorgänge problematisch waren, oder es nach außen kein SyncRoot-Object gab, das man für das Iterieren durch die Collection locken könnte. Auch hatte ich damals eine kleine Anwendung in WPF geschrieben, bei der ca. 20 Threads Listenoperationen durchführten. Ich kann mich nicht erinnern, dass das länger als ein paar Sekunden/Minuten gut ging und hatte dann selber noch etwas dran rumgebaut. Auch wenn ich selber nicht zufrieden war/bin, hat es mit der Collection bisher im 8h Betrieb keinerlei Probleme gegeben, wobei da die Listenänderungen meist vom Benutzer (UI-Thread) ausgegangen sind und die anderen Tasks größtenteils nur die Liste durchlaufen oder eventuell beim CollectionChanged-Event bestimmte Aktionen ausgeführt haben. Diese Konstellation hat sich etwas geändert und deshalb sind mir bei "Belastungstests" Deadlocks aufgefallen.
Daher dachte ich, dass es vielleicht Sinn macht hier mal im Forum nachzufragen, wie eine Implementierung solch einer Collection am sinnvollsten ist.

Gruß
Thomas

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 11 Jahren

Hallo,

ich wollte hiermit vom aktuellen Status dieses genannten Problems berichten, falls jemand das Thema verfolgt:

Obwohl alles über Dispatcher.Invoke geht, konnte ich beim Öffnen von Fenstern mehrfach erkennen, das z.B. 3 Elemente in der ListView angegezeigt werden, obwohl in der Collection definitiv nur 2 vorkommen (ein Objekt wurde in der ListView mehrfach eingefügt, lässt sich auch an der doppelten blauen Selektierung erkennen).

Ich vermute das Problem gefunden zu haben. In meiner SyncObservableCollection<T> hole ich mir den Dispatcher mit Dispatcher.CurrentDispatcher:

public class SyncObservableCollection<T> : ObservableCollection<T>
{
    private Object syncRoot;
    private Dispatcher dispatcher;    

    public SyncObservableCollection()
    {
        syncRoot = new Object();
        dispatcher = Dispatcher.CurrentDispatcher;
    }

    //...

In der Model-Schicht habe ich so eine Art Hauptklasse (Singleton). Beim Starten der Anwendung wird alles vom UI-Thread initialisiert und auch diverse SyncObservableCollections werden angelegt. (Erst später greifen darauf andere Threads zu)
Es gibt natürlich teils auch Klassen, die wiederum eine bzw. mehrere SyncObservableCollections nutzen. Neue Instanzen hatte bisher aber immer der Benutzer über die Benutzeroberfläche (UI-Thread) erzeugt. D.h. auch der Konstruktor der SyncObservableCollection wurde vom UI-Thread aufgerufen.

Bei meinem ersten Beitrag hatte ich erwähnt:

Ein Gerät hat eine Liste aus Teilnehmer. Ein Teilnehmer hat eine Liste aus Features.

Genau hier scheint das Problem zu liegen, denn neue Teilnehmer werden von einem Task/Thread eingefügt, der mit der Hardware kommuniziert. Diese Teilnehmer-Klasse beinhaltet eine SyncObservableCollection mit Features, aber diese Collection wird dann nicht vom UI-Thread angelegt. Damit holt sich Dispatcher.CurrentDispatcher leider nicht den Dispatcher vom UI-Thread, was man an unterschiedlichen Thread-Ids "dispatcher.Thread.ManagedThreadId" erkennt.

Um das Problem zu beheben, hatte ich zunächst folgendes getestet:

public class SyncObservableCollection<T> : ObservableCollection<T>
{
    private Object syncRoot;
    private static Dispatcher dispatcher;    

    public SyncObservableCollection()
    {
        syncRoot = new Object();
        
        if(dispatcher == null)
            dispatcher = Dispatcher.CurrentDispatcher;
    }

    //...

Da ich mir sicher bin, dass der UI-Thread als erstes diesen Konstruktor aufruft, sollte der dispatcher somit danach immer richtig gesetzt sein.
Leider habe ich nicht beachtet, dass das eine generische Klasse ist und es mit der Klassenvariable so nicht funktioniert, denn die ist bei jedem Typ trotzdem anders. Ich habe mir nur mal als Workaround folgendes zusammengeschustert:

public static class SyncObservableCollectionDispatcherHelper
{
    private static Dispatcher dispatcher;
    public static Dispatcher Dispatcher
    {
        get
        {
            if (dispatcher == null)
                dispatcher = Dispatcher.CurrentDispatcher;

            return dispatcher;
        }
    }
}

public class SyncObservableCollection<T> : ObservableCollection<T>
{
    private Dispatcher dispatcher;
    private Object syncRoot = new Object();
        
    public SyncObservableCollection()
    {
        syncRoot = new Object();
        dispatcher = SyncObservableCollectionDispatcherHelper.Dispatcher;
    }

    //...

Zumindest ist jetzt der Dispatcher immer der des UI-Threads, vorausgesetzt man sorgt dafür, dass der Konstruktor der Collection beim ersten Mal vom UI-Thread aufgerufen wird.
Die Lösung ist deshalb eine gefährliche Sache.

Am liebsten wäre mir natürlich sowas in der Art:

public class SyncObservableCollection<T> : ObservableCollection<T>
{
    private Dispatcher dispatcher;
    private Object syncRoot = new Object();

    public SyncObservableCollection()
    {
        syncRoot = new Object();

        Thread uiThread = Thread.GetMainThread(); //pseudo
        dispatcher = Dispatcher.FromThread(uiThread);
    }

    //...

Leider hab ich noch keine passenden Hinweise im WWW gefunden, ob es eine einfache Möglichkeit gibt an den Hauptthread der Anwendung zu kommen.

Bzgl. LINQ wollte ich auch nochmal nachhaken:
Ich will einfach nicht überall in den Properties Kopien der Collections erstellen, sondern mit den "originalen" Collections (observable) arbeiten, um eventuell auch an manchen Stellen das CollectionChanged-Event zu registrieren.

Folgender schnell eingetippter Code als Beispiel:

public class Teilnehmer
{
    private SyncObservableCollection<Feature> features = new SyncObservableCollection<Feature>();
    public SyncObservableCollection<Feature> Features
    {
        get { return features; }
    }
}

public class Feature
{
    public String Name { get; set; }
}

public class Test
{
    private SyncObservableCollection<Teilnehmer> teilnehmer = new SyncObservableCollection<Teilnehmer>();

    public Test()
    {
        Task.Factory.StartNew(() => { TeilnehmerUndFeatureErzeugung(); });
        Task.Factory.StartNew(() => { MachWasMitForeach(); });  
        Task.Factory.StartNew(() => { MachWasMitLinq(); });
    }

    public void TeilnehmerUndFeatureErzeugung()
    {
        Random random = new Random();

        while(true)
        {
            Teilnehmer t = new Teilnehmer();
            teilnehmer.Add(t);

            for (int i = 0; i < random.Next(10); i++)
                t.Features.Add(new Feature { Name = String.Format("Feature {0}", i) });
        }
    }

    public void MachWasMitForeach()
    {
        while(true)
        {
            lock (teilnehmer.SyncRoot)
            {
                foreach (Teilnehmer t in teilnehmer)
                {
                    lock (t.Features.SyncRoot)
                    {
                        foreach (Feature f in t.Features)
                        {
                            //Mach was...
                        }
                    }
                }
            }
        }
    }

    public void MachWasMitLinq()
    {
        while (true)
        {
            var features = from t in teilnehmer
                            from f in t.Features      // where... 
                            select f;

            foreach (Feature f in features) //Das führt natürlich zu einer InvalidOperationException
            {
                //Mach was...
            }
        }
    }
}

Der Code-Teil mit dem foreach macht keinerlei Probleme. Den finde ich allerdings etwas unleserlich, besonders wenn man noch diverse if-Bedingungen einbaut.

Für solche Fälle hat mir LINQ schon immer ganz gut gefallen. Aber hier hat man es leider mit mehreren Threads zutun. Man könnte den Code-Teil mit einem lock(teilnehmer.SyncRoot) umschließen, jedoch fehlt dann noch ein lock von der Features-Collection.
Gibt es in LINQ keine Möglichkeit das thread safe zu gestalten?

Bitte berichtigt mich, wenn ich hier irgendwas falsch interpretiere. Vielleicht hat ja der ein oder andere noch ein paar Tipps über die ich ich mich sehr freuen würde.

Viele Grüße
Thomas

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo Master15,

es liegt überhaupt nicht in der Zuständigkeit der SyncObservableCollection, den richtigen Dispatcher zu ermitteln/erzeugen. Diese Klasse kann überhaupt nicht wissen, welches der richtige Dispatcher ist. Im Grunde liegt es noch nicht mal in ihrer Zuständigkeit, überhaupt zu dispatchen. Wenn die Klasse aus Komfortgründen dispatchen soll, dann sollte der Benutzer der Klasse den richtigen Dispatcher übergeben, z.B. als Konstruktorparameter.

Aber auch das alles wird in der FAQ behandelt, siehe den ersten Absatz in "Spezielle Probleme" und Eleganteste Art aus Worker-Thread auf Controls zugreifen [generell Kontrollfluss zwischen Threads]. Deshalb nochmal meine Bitte, das Thema Dispatcher nicht weiter zu behandeln, da alles notwendige in der FAQ steht. (Wenn du findest, dass was fehlt, nimm Kontakt zum Team auf.)

Was die Synchronisation bei Linq angeht, hast du ja erkannt, dass (mindestens) ein lock fehlt. Wenn ich es richtig sehe, möchtest du über alle alle Features über alle Teilnehmer iterieren. Dann müsstest du wohl auch alle Sync-Objekte aller Feature-Collections aller Teilnehmer sperren. Oder eben doch mit Kopieren der Collections arbeiten.

herbivore

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 11 Jahren

Hallo herbivore,

danke für deine Antwort.

es liegt überhaupt nicht in der Zuständigkeit der SyncObservableCollection, den richtigen Dispatcher zu ermitteln/erzeugen. Diese Klasse kann überhaupt nicht wissen, welches der richtige Dispatcher ist. Im Grunde liegt es noch nicht mal in ihrer Zuständigkeit, überhaupt zu dispatchen. Wenn die Klasse aus Komfortgründen dispatchen soll, dann sollte der Benutzer der Klasse den richtigen Dispatcher übergeben, z.B. als Konstruktorparameter.

Also das mit dem Konstruktor-Parameter hatte ich mir auch schon gedacht und vermutet, dass das als Einwand kommen wird. Eigentlich finde ich den Dispatcher in der Collection einfach nur nervig. Der hat da nichts zu suchen. Am liebsten würden ich den komplett rausschmeißen, wäre da nicht WPF mit den Bindings, wenn man doch mal die Collection direkt in der View nutzen will (was in MVC erlaubt ist). Freilich könnte man eine Art Schicht (bzw. ViewModel) nochmal zwischenschalten, dort dann das CollectionChanged der Collection abfangen und die Steuerelemente bzw. eine ObservableCollection (die an Steuerelemente gebunden ist) dann über Dispatcher.Invoke befüllen.
Aber wenn man mit vielen Collections arbeitet, wird das im Code kein Spaß mehr (bzw. copy&paste). Ich frage mich mittlerweile ernsthaft, ob man eventuell ein Art Konverter bzw. eigene CollectionView zwischenschalten könnte, die sich um das Dispatchen kümmert.

Aber auch das alles wird in der FAQ behandelt, siehe den ersten Absatz in "Spezielle Probleme" und Eleganteste Art aus Worker-Thread auf Controls zugreifen [generell Kontrollfluss zwischen Threads]. Deshalb nochmal meine Bitte, das Thema Dispatcher nicht weiter zu behandeln, da alles notwendige in der FAQ steht. (Wenn du findest, dass was fehlt, nimm Kontakt zum Team auf.)

Hab ich mir durchgelesen, ist informativ. Hilft mir aber nur bedingt weiter. Werde das Forum am nächsten Wochenende nochmal durchforsten.

Was die Synchronisation bei Linq angeht, hast du ja erkannt, dass (mindestens) ein lock fehlt. Wenn ich es richtig sehe, möchtest du über alle alle Features über alle Teilnehmer iterieren. Dann müsstest du wohl auch alle Sync-Objekte aller Feature-Collections aller Teilnehmer sperren.

Und genau darauf bezieht meine Frage. Wie lassen sich die Sync-Objekte der Feature-Collections jeweils sperren?

So etwas in der Art meine ich:

var features =  linqLock(teilnehmer.SyncRoot)
                from t in teilnehmer
                linqLock(t.Features.SyncRoot)
                from f in t.Features
                select f;

foreach (Feature f in features)
{
    //Mach was...
}

Ich hab mal vorsichtshalber mit lock(...), sondern linqLock(...) geschrieben, damit man das nicht fehlinterpretiert.
Ich habe den Eindruck, dass LINQ für so etwas nicht ausgelegt ist und man doch wieder auf for/foreach zurückgreifen muss.

Oder eben doch mit Kopieren der Collections arbeiten.

Finde ich eine Notlösung, die ich so nicht akzeptieren kann/will (u.a. wegen Observer-Pattern).

Viele Grüße
Thomas

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo Master15,

auch bei thread-sicheren Collections ist die Enumeration normalerweise nicht thread-sicher. Deshalb muss man Enumerationen (Schleifen u.ä.) komplett und explizit mit einem lock umschließen(*). Ich bin kein Linq-Spezialist, aber ich wüste keinen Grund, warum das dort anders sein sollte.

Wenn ich Recht habe, dann müsstest du das gesamte Linq-Statement mit den erforderlichen locks umschließen. Wenn du das tust, braucht du auch nicht zu foreach zurückzugehen.

herbivore

(*) Um das (einfach) tun zu können, braucht man das lock-Objekt der thread-sicheren Collection. Deshalb bin ich auch so ein Fan von lock(this), siehe Sollte man lock this vermeiden? [==> Microsoft sagt ja, herbivore sagt nein]. Aber das nur nebenbei.