List<> und Threadsafety



  • Hi,

    ich habe einen Thread in dem ich eine Liste verwalte und ändere, und einen anderen
    Thread, der auf diese Liste zugreift.

    In bestimmten fällen kann das in meinem Fall zu Ausnahmen führen, die aber für
    mich nicht weiter schlimm sind.

    Bisher fange ich das über einen try-catch Block ab. Sollte ich lieber sowas wie
    ConcurrentBag<> benutzen? Eigtl bin ich mit meiner List aber ganz glücklich :).

    Grüße
    Huddi



  • Ja du solltest lieber die dafuer vorgesehen Datenstrukturen nehmen wenn diese dir schon die passende Sicherheit bieten.



  • Hi.

    Ohne Synchronisation von mehreren Threads auf die List zugreifen ist natürlich tödlich. Und es stört Dich nicht? 😮

    Eine Collection aus System.Collections.Concurrent kann helfen, muss aber nicht. Das kommt auf die Art des Bugs an und solche Collections können nur synchronisieren und schützen was intern abläuft. Methodenübergreifende Fehler, d.h. solche die durch mehrere Methodenaufrufe geschehen, kann die Collection nicht bemerken und abfangen.
    Beispiel:
    Thread 1: Einfügen eines Items X in die Collection
    Thread 2: Löschen von Item X
    Thread 1: Zugriff auf (nun nicht mehr vorhandenes) Item X

    Die Concurrent.Collections bieten aber immerhin Unterstützung wie die TryTake-Methode. Entsprechende Aufrufe müsstest Du dann eben gegenüber der List anpassen bzw Dein Programm abändern.

    Also. Erstmal rausfinden oder hier erklären wie sich Dein Problem äußert und wie es zu Stande kommt



  • µ schrieb:

    Ohne Synchronisation von mehreren Threads auf die List zugreifen ist natürlich tödlich. Und es stört Dich nicht? 😮

    Naja, ein bisschen schon. Deswegen frag ich ja hier :).

    µ schrieb:

    Also. Erstmal rausfinden oder hier erklären wie sich Dein Problem äußert und wie es zu Stande kommt

    Momentan habe ich keine Probleme. Das Programm funktioniert wunderbar auf diese
    Art.

    Ich berechne Daten in dem einen Thread und der andere Thread dient dazu das
    ganze dem User darzustellen (wie bei einem Computerspiel), deshalb ist es nicht
    schlimm, wenn ab und zu keine Daten ankommen.

    Was mich an ConcurrentBag stört ist, dass ich keine clear() Methode habe.
    Ich könnte natürlich statt clear eine Schleife benutzen, aber das ist bestimmt
    auch nicht so schön?

    Meine Frage ist also eher, ob ich noch andere Probleme erwarten kann, als die
    eine oder andere Exception. Oder ob dieses willentliche Exception produzieren
    so dermaßen ekelhaft ist, dass man es lieber lassen sollte 🤡 .



  • @huddi
    Es kann so-gut-wie alles passieren wenn du unsynchronisiert auf Objekte zugreifst, die nicht Thread-safe sind.
    C# bietet da vermutlich etwas mehr Garantien als C++, aber grundsätzlich sollte man so etwas NIE NIEMALS machen.

    Skizzier mal inetwa was du da machst. Ich vermute dass es da ne halbwegs elegante Möglichkeit gibt, die 100% sicher ist, und keinen merkbaren Performance-Nachteil hat.

    Die fertigen Concurrent-Collections sind nett, wenn sie genau das können was man braucht (bzw. wenn man das was man braucht schön mit dem abbilden kann was die fertigen Klassen können). Oft braucht man aber etwas anderes, und dann synchronisiert man eben selbst.



  • Hallo,

    wenn du deinen Code nur minimal anpassen möchtest, kann ich dir meine ThreadSafeList<T> empfehlen. Sie lässt sich verwenden wie die normale List<T> aus dem Framework, bietet intern aber bereits Synchronisation.



  • Hallo huddi,

    ich habe dieses Problem mit einer einfachen Warte-Routine gelöst. Wird auf die Liste zugegriffen, muss ein Flag gesetzt werden. Ist der Zugriff zuende, wird dieses Flag wieder zurückgesetzt. Will nun ein anderer Thread auf die Liste zugreifen, wartet dieser in einer while-Schleife, bis das Flag zurückgesetzt wurde. So kannst du immernoch deine normale Liste benutzen und musst nur ein paar Methoden den Zugriff überlassen.

    Hier ein Beispiel:

    class SyncTest
    	{
    		private List<object> liste = new List<object>();
    		private bool wirdVerwendet = false;
    
    		public void Add(object obj)
    		{
    			//Warten bis Liste nicht mehr verwendet wird
    			while (wirdVerwendet)
    			{
    				Thread.Sleep(1);
    			}
    
    			//Flag setzen und Objekt hinzufügen
    			wirdVerwendet = true;
    			liste.Add(obj);
    			wirdVerwendet = false;
    		}
    
    		public object Get(int index)
    		{
    			object obj = null;
    
    			//Warten bis Liste nicht mehr verwendet wird
    			while (wirdVerwendet)
    			{
    				Thread.Sleep(1);
    			}
    
    			//Flag setzen und Objekt abrufen
    			wirdVerwendet = true;
    			obj = liste[index];
    			wirdVerwendet = false;
    
    			return obj;
    		}
    	}
    


  • Ape schrieb:

    Hallo huddi,

    ich habe dieses Problem mit einer einfachen Warte-Routine gelöst. Wird auf die Liste zugegriffen, muss ein Flag gesetzt werden. Ist der Zugriff zuende, wird dieses Flag wieder zurückgesetzt. Will nun ein anderer Thread auf die Liste zugreifen, wartet dieser in einer while-Schleife, bis das Flag zurückgesetzt wurde. So kannst du immernoch deine normale Liste benutzen und musst nur ein paar Methoden den Zugriff überlassen.

    Und wenn eine der List<T>-Methoden eine Exception wirft, dann bleibt "wirdVerwendet" halt auf true und alles hängt fröhlich in while-Schleifen rum 😉 Thread-Safe ist der Code auch nicht.
    Gibt's einen plausiblen Grund, warum du nicht das lock-Statement benutzt hast?



  • Ape schrieb:

    ...

    👍 Besser kann man sich keinen Deadlock bauen.



  • @GPC:

    Das ist ja nur Beispiel-Code 😉
    Man sollte natürlich einen Try-Catch-Block um den Zugriff setzen. Aber eigentlich macht es keinen Sinn das Programm weiterlaufen zu lassen, wenn hierbei ein Fehler auftreten solle. 😛

    Von "lock" habe ich bis jetzt nur gehört, dass es das Programm ziemlich ausbremst. Wenn du es verwenden willst, solltest du dich vielleicht über die Vor- und Nachteile erkundigen. 🙂



  • Ape schrieb:

    Von "lock" habe ich bis jetzt nur gehört, dass es das Programm ziemlich ausbremst. Wenn du es verwenden willst, solltest du dich vielleicht über die Vor- und Nachteile erkundigen. 🙂

    Der Nachteil an deinem Code ist halt, dass er nicht funktioniert.



  • Ape schrieb:

    Aber eigentlich macht es keinen Sinn das Programm weiterlaufen zu lassen, wenn hierbei ein Fehler auftreten solle. 😛

    Die anderen Threads wären sicher gerne weitergelaufen, wenn die Exception in einem einzelnen Thread sie nicht blockieren würde 😉

    Von "lock" habe ich bis jetzt nur gehört, dass es das Programm ziemlich ausbremst. Wenn du es verwenden willst, solltest du dich vielleicht über die Vor- und Nachteile erkundigen. 🙂

    Oh, ich will dir nicht zu Nahe treten, aber machen wir es doch andersherum: Du erkundigst dich erstmal über lock und dann diskutieren wir die Vor- und Nachteile deiner "Lösung" 😉
    Zum Anfang wäre das hier geeigneter Lesestoff: http://blog.coverity.com/2014/02/12/how-does-locking-work/



  • inflames2k schrieb:

    👍 Besser kann man sich keinen Deadlock bauen.

    Ich verwende dieses Verfahren bei einer TCP/IP-Kommunikation und hatte bisher noch keine Probleme damit. Der Fall, dass ein Thread warten muss, tritt ja eigentlich nur selten auf, da Threads nicht all zu oft auf ein Objekt zur exakt selben Zeit zugreifen. Kommt natürlich auf den Anwendungsfall an. Und falls er dann mal doch in die Schleife gerät, sollte er ja im Regelfall nach dem ersten Schleifen-Durchlauf weitermachen. Wenn 1ms zu viel sein sollte, kann man das Thread.Sleep auch ganz weglassen.



  • Ape schrieb:

    Ich verwende dieses Verfahren bei einer TCP/IP-Kommunikation und hatte bisher noch keine Probleme damit.

    Natürlich, es geht so lange gut, bis es eben knallt.

    Sobald dein Code ein wenig unter Last gerät, schmiert er ab. Immer mal wieder, unvorhersagbar.

    Was meinst du, warum es fertige Klassen für Synchronisierung gibt? Weil man sie nicht braucht, sondern sich mit einem bool und einer Schleife das gleiche eben mal hinbasteln kann?



  • Sorry Ape aber Dein Code ist wirklich übel. Am besten direkt wegwerfen und erstmal in Threachsychronisation reinlesen.

    Ape schrieb:

    Man sollte natürlich einen Try-Catch-Block um den Zugriff setzen.

    Und Exception-Handling hast Du auch nicht verstanden.



  • Wie gesagt habe ich mich selbst mit "lock" noch nicht viel befasst. Ich will ja damit nicht sagen, dass es schlecht ist, sondern dass man es sich mal genauer anschauen sollte, bevor man es einfach verwendet.

    Ihr erwähnt dauernd, dass es mit dem Flag nicht auf Dauer funktioniert. Kann bitte einer von euch auch erklären warum? Habe zwar schon einen Stresstest bei der TCP/IP-Kommunikation getestet und nichts festgestellt, aber ich würde gerne mal wissen, was da schieflaufen kann. 😕

    Ich will ja nicht behaupten, dass mein Verfahren das einfachste und beste ist und alle anderen kacke und unnötig. 😃 Bin ja selbst noch am Lernen und wär über andere überzeugende Vorschläge dankbar, damit ich meinen Code anpassen kann. Aber einfach nur sagen, dass es nicht klappt, ist nicht sehr hilfreich. 😞



  • Ape schrieb:

    Das ist ja nur Beispiel-Code 😉

    Ich halte das für ein Anti-Beispiel Code. So sollte man es auf keinen Fall machen. Vor allem weil der Code definitiv nicht thread-safe ist, wie andere schon erwähnten.

    Ape schrieb:

    Ich verwende dieses Verfahren bei einer TCP/IP-Kommunikation und hatte bisher noch keine Probleme damit.

    So ziemlich alles funktioniert _bis_ zu dem Moment wo es dann schiefgeht. Leider verwechseln die meisten möchtegern-Progger ein "ist bisher noch nicht schiefgegangen" mit "funktioniert korrekt".

    Das ist wie der Fallschirmspringer der ohne Schirm rausspringt. Die ersten 90 Sekunden funktioniert das auch problemlos. Trotzdem bezweifele ich das er es richtig gemacht hat...



  • Ape schrieb:

    Ihr erwähnt dauernd, dass es mit dem Flag nicht auf Dauer funktioniert. Kann bitte einer von euch auch erklären warum?

    Zwei Threads. Der eine ruft Add auf, der andere Get. Beide laufen an deiner Synchronisierungsschleife vorbei (wirdVerwendet ist ja false). Beide Threads greifen gleichzeitig auf die Liste zu -> Peng.

    Selbst wenn dein Programm auf einem einzigen Kern ausgeführt wird, und die Threads somit nicht gleichzeitig ablaufen können, kann dir das passieren:

    Thread 1 ruft Add auf, läuft an der Schleife vorbei.
    Wechsel nach Thread 2, bevor Thread 1 wirdVerwendet setzen kann.
    Thread 2 ruft Get auf, läuft auch an der Schleife vorbei, setzt wirdVerwendet, und durchsucht die Liste. Mittendrin wieder Wechsel zu Thread 1.
    Thread 1 setzt auch wirdVerwendet, und ändert die Liste.
    Wechsel zu Thread 2, der mit dem Zugriff auf eine Liste fortfährt, die zwischenzeitlich geändert wurde -> Peng.



  • Okay, also ein lock wäre dann genauso safe wie die Verwendung von zum Beispiel
    ConcurrentQueue? Irgendwie komm ich ohne den Indexzugriff bei einer Liste nicht
    zurecht.

    Im Konkreten Fall habe ich ein Objekt List<List<Point>>, das übergeben werden
    soll. ConcurrentQueue<List<Point>> wäre dann aber auch unsinnig, wenn die
    innere Liste gleichzeitig verändert und darauf zugegriffen wird, oder?



  • Ape schrieb:

    @GPC:

    Das ist ja nur Beispiel-Code 😉
    Man sollte natürlich einen Try-Catch-Block um den Zugriff setzen. Aber eigentlich macht es keinen Sinn das Programm weiterlaufen zu lassen, wenn hierbei ein Fehler auftreten solle. 😛

    Von "lock" habe ich bis jetzt nur gehört, dass es das Programm ziemlich ausbremst. Wenn du es verwenden willst, solltest du dich vielleicht über die Vor- und Nachteile erkundigen. 🙂

    NEIN DAS IST KEIN BEISPIELCODE, ES IST FALSCH, UND ES IST GEFÄHRLICH.

    Wenn man keine Ahnung von etwas hat, dann sollte man es bleiben lassen. Und wenn man schon unbedingt solchen Unfug bauen muss, dann sollte man ihn gefälligst nicht in Foren posten.


Anmelden zum Antworten