List<> und Threadsafety



  • µ 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.



  • Ape schrieb:

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

    Der erste "Bug" ist, dass du nicht volatile verwendest. Dadurch muss der Compiler an der Stelle wo du die Variable zuweist gar keine echte Zuweisung machen. Unter bestimmten Bedingungen, z.B. dass in dem Teil zwischen "wirdVerwendet = true" und "wirdVerwendet = false" keine Exceptions geworfen werden können, kann der Compiler beide Zuweisungen einfach weglassen. Der Compiler "sieht" dann beim Optimieren:
    * Wenn wir aus der Schleife rauskommen muss die Variable false sein, sonst wären wir nicht rausgekommen.
    * Dann schreiben wir true rein.
    * Dann machen wir was.
    * Dann schreiben wir wieder false rein.
    * Dazwischen liest keiner die Variable (da die Variable nicht volatile ist, müssen andere Threads nicht berücksichtigt werden).
    => Wir können die zwei Zuweisungen gleich ganz weglassen.

    Selbst mit volatile hast du aber das Problem, dass zwei Threads gleichzeitig in einer Funktion warten könnten, und dann beide gleichzeitig "sehen" dass "wirdVerwendet" false ist, und beide gleichzeitig weiterlaufen.
    Und dann greifen erst wieder zwei (oder evtl. sogar mehr) Threads gleichzeitig auf die Collection zu.
    Zusätzlich setzt dann noch der Thread der als erstes bei "seinem" "wirdVerwendet = false" ankommt die Variable auf false, und weitere Threads die warten laufen ebenfalls los.

    Um das zu lösen brauchst du dann sog. atomare Operationen, und zwar im speziellen eine "CAS" Operation (Compare And Swap). Im .NET Framework sind diese atomaren Operationen in der Interlocked Klasse zu finden. Die "CAS" Funktion heisst dort Interlocked.CompareExchange.
    http://msdn.microsoft.com/en-us/library/system.threading.interlocked.compareexchange.aspx

    Und wenn du damit dann alle Bugs beseitigt hast, dann hast du nen Code der mit an Sicherheit grenzender Wahrscheinlichkeit langsamer ist als lock() .
    Weil lock() im Endeffekt nämlich auch nix anderes macht. Nur dass es von Profis entwickelt wurde und sehr gut optimiert ist.

    => Lass den Unsinn, und verwende die fertigen Funktionen die genau dafür gemacht wurde.



  • huddi schrieb:

    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.

    Wenn dem wirklich so ist, verweise ich nocheinmal auf meinen ersten Beitrag. - Dort habe ich meine ThreadSafeList<T> vorgeschlagen, die sich genauso verwenden lässt die die List<T> des Frameworks.



  • inflames2k schrieb:

    Dort habe ich meine ThreadSafeList<T> vorgeschlagen, die sich genauso verwenden lässt die die List<T> des Frameworks.

    Deine ThreadSafeList ist bloss leider überhaupt nicht thread-safe.

    Nur mal eines von vielen Beispielen:

    public bool Remove(T item)
             {
                 int index = this.IndexOf(item);
                 if (index == -1)
                     return false;
    
                 this.RemoveAt(index);
                 return true;
             }
    

    Echt jetzt?

    Überleg dir mal was passiert wenn die Liste {1, 2, 3} enthält, und dann zwei Threads gleichzeitig Remove(2) machen.

    Oder ne Liste die ein einziges Element "1" enthält. Bei zwei zeitlich getrennten Remove-Aufrufen liefert der erste true zurück und der zweite false . Bei zwei gleichzeitigen Remove-Aufrufen wird einer true zurückliefern, der zweite kann aber ne ArgumentOutOfRangeException Exception werfen.

    Also auch an dich: bitte lies dich etwas genauer in das Thema ein, und poste erst dann gute Ratschläge, wenn du dir wirklich sicher bist es verstanden zu haben.

    Und BTW: http://stackoverflow.com/questions/251391/why-is-lockthis-bad


Anmelden zum Antworten