Tasks Synchronisieren mit Event und CriticalSection

Für Fragen von Einsteigern und Programmieranfängern...
Antworten
Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Nimral »

Diesmal bitte ich euch, den folgenden "finsteren Plan" zu begutachten. Die Umsetzung ist doch etwas aufwändig (vor Allem der Test), ich möchte, weil ich bisher noch nie so komplexe Multitasking Anwendungen hatte möglichst keinen Fehler im Grundkonzept machen, ich muss bis Mittwoch fertig sein und kann mir keine größeren Fehler im Ansatz erlauben. Ich danke euch wieder mal ausdrücklich, dass ihr euch die Zeit nehmt, das sind normalerweise Dinge dioe man mit einem erfahrenen Kollegen bespricht, aber blöderweise bin ich der Einzige Pascal Kämpfer hier, drum muss ich euch bitten. Damit ihr nicht das Gefühl habt, ich missbrauche euch um meine Arbeit zu machen schreibe ich haarklein auf, wie ich es machen möchte.

Zielplattformen: WIndows und Linux. Lazarus: 2.2.0 RC2, FPC: 3.2.2

Ausgangssituation: durch mein Programm fliegen immer wieder kleine Tasks, ausgelöst durch Netzwerkereignisse, ich nennen sie Netzwerk-Tasks.. Sie machen kurz (gemessen: <50ms) was, schieben paar Daten ins Netz zurück, und terminieren sich dann selbst mit freeonterminate=true. Datenbasis ist eine recht große TFPGObjectList<T>. Es werden definitiv immer eine einstellige Zahl gleichzeitige Netzwerk-Tasks sein, da die Lösung nur relativ wenige Clients bedienen muss. Wenns mal 10 werden, ist das viel. Die Netzwerk-Tasks lesen diese Daten ausschließlich.

Dann gibt es einen einzigen weiteren Task, ich nenne ihn Lese-Task, der die TFPGObjectList<T> von einer Datei liest. Wie oft die Datei verändert wird ist unbekannt, ich rechne mal mit einem Minimalintervall von einigen Minuten und einem typischen Intervall von einer Stunde oder so. Der Task läuft in seiner execute Funktion in der üblichen while not terminated Schleife und checkt das Dateidatum. Wenn es sich verändert hat, liest er neu. Um das Umschalten der TFPGObjectList<T> so schnell wie möglich zu machen baut der Lesetask erst eine neue TFPGObjectList<T> im Speicher auf, wenn er damit fertig ist, ersetzt er die alte Liste mit einer direkten Zuweisung durch die Neue, und gibt die alte Liste danach frei. Den doppelten Speicherbedarf kann ich locker verkraften.

Um jetzt dafür zu sorgen, dass kein Lesetask aktiv ist in dem Augenblick, wo das Ersetzen stattfindet, habe ich mir folgendes Konzept zurechtgelegt:

1.) ich baue mir einen mit einer CriticalSection geschützten Zähler, jeder Netzwerk-Task zählt ihn beim Start eins hoch und vor dem Beenden eins runter. Dazu gibt es eine Counter property und eine "incCounter" und eine "decCounter" Methode, die beide mit der selben CriticalSection abgesichert sind. Zusätzlich führe ich einen TEvent "CanUpdate" mit (Reset=manual), CanUpdate wird innerhalb von incCounter und decCounter innerhalb der CriticalSection true gesetzt, wenn der Zähler von 1 nach 0 wechselt, und gelöscht wenn der Zähler von 0 nach 1 wechselt. Initialisiert wird er auf "true".

2.) Der Reader-Task wird als allererstes gestartet, noch bevor ich den Netzwerksocket aktiviere, er hat also während des ersten Datenlesens keine Netzwerk-Tasks zu befürchten. Der Dreizeiler der am Ende die bestehende Liste gegen die neu gelesene Liste tauscht ist ebenfalls mit der CriticalSection abgesichert. Unmittelbar vor dem Betreten der CriticalSection steht ein WaitFor auf den CanUpdate Event. Außerdem ruft der Reader-Task am Ende jedes Durchlaufs der Execute Schleife per Synchronize ein Ereignis "onDataParsed" auf. Im Handler, der sich im Mainthread befindet, wird der Netzwerkserver gestartet, sofern er nicht schon läuft. Das wiederum gibt die IP Ports und damit auch die Netzwerk-Tasks frei.

3.) Umd as System zu stoppen, beende ich zuerst den Reader- und den Netzwerk-Task und warte dann auf CanUpdate. Sobald es da ist, kann ich das Programm beenden.

Ich erwarte mir folgendes Systemverhalten: beim Programmstart stellt der Reader fest, dass sich die Datei aus seiner Sicht verändert hat, liest die Daten in die temporäre TFPGObjectList<T>, rauscht dann über CanUpdate.WaitFor (da der Event per Init true gesetzt ist) in die CriticalSection (wo kein anderer Task sein kann, da das Netzwerk zu diesem Zeitpunkt noch keinen entgegen nimmt), und weist die temporäre Liste an TFPGObjectList<T> zu. Dann signalisiert er seinen onDataParsed, und dreht die nächste Check-Schleife auf geändertes Dateidatum. Meistens wird sich nicht geändert haben, dann lege ich ihn per Sleep/Yield für eine Weile schlafen. Hat sich was geändert, macht er den selben Durchlauf wieder, sofern CanUpdate weiterhin gesetzt ist, was genau so lange der Fall ist bis ein oder mehrere Netzwerk-Tasks laufen. Sollte das der Fall sein, fängt er sich in CanUpdate.Waitfor bis sie alle beendet sind. Sollten noch ein paar Nachzügler dazu kommen, wartet er eben weiter, bis der TaskCounter irgendwann auf null geht. Dann wird CanUpdate true, er betritt die CriticalSection und tauscht die Daten, eventuell genau jetzt einlaufende neue Netzwerk-Tasks fangen sich in der CriticalSection beim Versuch, den TaskCounter zu inkrementeren bis das Listen-Tauschen durch ist, was nur einige Millisekunden dauern wird.

Sieht irgendwer einen Denkfehler? Vor allem habe ich TEvent (TRTLEvent?) ein bestimmtes Verhalten unterstellt: ich kann ihn, wenn er im manuellen Modus ist wie eine rote Ampel setzen und rücksetzen und so oft ich möchte abfragen. Keine Ahnung, ob das Eventmodell der beiden Zielplattformen das hergibt. Weiters unterstelle ich, dass die Netzwerk-Tasks, die schon gestartet sind, unbeindruckt zu Ende laufen, wenn ich den Port-Server beende. Hier muss ich eventuell noch Code reinbauen damit er erst mal nur keine neuen Sessions entgegen nimm, die schon laufenden Sessions aber noch zuende bringt. Am Einfachsten wird es sein, das Netzwerk erst zu beenden, wenn CanUpdate true ist.

Was meint ihr? Hat dieses einfache Konzept Chancen, dass es unter Windows und Linux so funktioniert wie ich es mir zurechtgelegt habe?

Ich habe folgende Unschönheiten ausgemacht, die ich zu tolerieren bereit bin:

- wenn ein Update einläuft, brauche ich für kurze Zeit den zweifachen Speicher für TFPGObjectList<T>
- wenn jemand das Programm böswillig mit Abfragen bombardiert, kommt der Reader-Task eventuell nicht zum Zug
- wenn jemand das Programm böswillig mit Abfragen bombardiert, kann ich das Programm nicht beenden

HG, Armin

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Socke »

Nimral hat geschrieben:
Fr 21. Jan 2022, 19:33
1.) ich baue mir einen mit einer CriticalSection geschützten Zähler, jeder Netzwerk-Task zählt ihn beim Start eins hoch und vor dem Beenden eins runter. Dazu gibt es eine Counter property und eine "incCounter" und eine "decCounter" Methode, die beide mit der selben CriticalSection abgesichert sind.
Hier brauchst du keine CriticalSection. InterlockedIncrement ist da AFAIK schneller.
Nimral hat geschrieben:
Fr 21. Jan 2022, 19:33
Dann gibt es einen einzigen weiteren Task, ich nenne ihn Lese-Task, der die TFPGObjectList<T> von einer Datei liest.
Du musst den gesamten Zugriff auf die Liste und deren Elemente mit der CriticalSection absichern. D.h. die darin enthaltenen Elemente müssen innerhalb der CriticalSection abgearbeitet oder aus der Liste entfernt werden. Gerade für die darin enthaltenen Elemente lebt es sich am einfachsten, wenn alle Referenzen auf ein Element ausschließlich in einem einzigen Thread zu finden sind. Damit ist immer eindeutig geklärt, wer auf das Objekt gerade zugreifen darf.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von af0815 »

Mir fällt nur eines ins Auge, die kleinen Netzwerktasks würde ich in einer Liste sammeln und mich nicht auf das FreeOnTerminate verlassen. Sammeln und Recyceln. Damit verlierst du auch nicht die Kontrolle. Wenn ein Netzwerktask nicht sauber im Protokoll terminiert, so kann der Socket länger hängen, hast du kein Handle auf irgendwas, hast du auch keine Kontrolle, falls wer 'flooded'. In einer Liste gesammelt, hat man zuminest die Kontrolle ,bzw. indikation falls was schief läuft.

Bei den CriticalSektions peinlichst auf abbruch bedingungen und exception catching achten, ein blöder deadlock ist ein Killer. Das habe ich schon schmerzhaft erfahren.

Und Linux und Windows sind gerade bei den Events nicht so 100pro gleich.
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Benutzeravatar
Winni
Beiträge: 1577
Registriert: Mo 2. Mär 2009, 16:45
OS, Lazarus, FPC: Laz2.2.2, fpc 3.2.2
CPU-Target: 64Bit
Wohnort: Fast Dänemark

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Winni »

Hi!

Hat af0815 schon implizit gesagt, aber nochmals:

Was ist, wenn die Netzwerkverbindung abbricht?

FreeOnTerminate passiert dann nie, wenn Du keinen TimeOut definiert hast.

Und nicht immer ist Google schuld bei Netzwerkausfällen.
Es kann auch sein, dass die Putzfrau den Stecker zum Server zieht, weil sie die Steckdose für den Staubsauger braucht. Alles schon passiert!

Winni

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von af0815 »

Es sind die kleinen Protokollfehler die dich umbringen. Ein gestörter Klient der Probleme hat, kann dich umbringen. Der fängt eine Kommunilation an, sturzt ab umd fängt wieder an. Ein Horror.
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Nimral »

Socke hat geschrieben:
Fr 21. Jan 2022, 20:39
Du musst den gesamten Zugriff auf die Liste und deren Elemente mit der CriticalSection absichern. D.h. die darin enthaltenen Elemente müssen innerhalb der CriticalSection abgearbeitet oder aus der Liste entfernt werden. Gerade für die darin enthaltenen Elemente lebt es sich am einfachsten, wenn alle Referenzen auf ein Element ausschließlich in einem einzigen Thread zu finden sind. Damit ist immer eindeutig geklärt, wer auf das Objekt gerade zugreifen darf.
Bist Du da sicher? Ich habe das Konzept extra so aufgebaut (der Lese/Update-Task geht nur an die Liste wenn kein einziger lesender Netzwerk-Task mehr arbeitet, und ich verhindere, dass ein neuer Netzwerk-Task an die Liste kommt bevor ich mit dem Update fertig bin) dass es niemanden mehr geben kann, der auf ein Listenobjekt zugreift, wenn ich die Liste anfasse.

Die Liste würde ich wie erwähnt vorab im Hintergrund aufbauen, und dann mit einem Zweizeiler in der CriticalSection umhängen:

Code: Alles auswählen


var NewList,temp: TMyList;

begin
NewList := TMyList.Create;
(.... neue Liste von Datei einlesen ...)
CanUpdate.WaitFor();   // feuert, wenn alle lesenden Tasks beendet sind
EnterCriticalSection(CriticalSection);
temp := CurrentList;
CurrentList := NewList;
DoneCriticalSection(CriticalSection);
FreeAndNil(temp);
Da ich alle Listen in der Hand habe als einziger aktiver Task seh ich da kein Problem.

Armin.

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Nimral »

Exception-handling kommt noch, ich muss mich erst einlesen wie das mit dem Try im Multitasking funktioniert, ich glaub da gibt es im Multitasking-Tutorial Infos dazu.

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Socke »

Nimral hat geschrieben:
Fr 21. Jan 2022, 22:29
Da ich alle Listen in der Hand habe als einziger aktiver Task seh ich da kein Problem.
Du solltest einfach darauf achten, dass die Threads sich nicht gegenseitig die Objekte streitig machen können. Wie du das machst, bleibt dir überlassen.
Nimral hat geschrieben:
Fr 21. Jan 2022, 22:31
Exception-handling kommt noch, ich muss mich erst einlesen wie das mit dem Try im Multitasking funktioniert, ich glaub da gibt es im Multitasking-Tutorial Infos dazu.
Das Exception-Handling funktioniert wie immer mit try..finally und try..except. Es gibt nur einen kleinen Unterschied: Führt die Exception dazu, dass die Execute-Methode verlassen wird, wird das Exception-Objekt in der Eigenschaft FatalException abgelegt und kann dann dort abgefragt werden. Das Programm wird hier nicht beendet (im Mainthread passiert genau das); es gibt also keinen Exception-Default-Handler.
Böse ist hier die Kombination von Exceptions mit FreeOnTerminate, da dann das Exception-Objekt nicht mehr freigegeben wird und du ein Memory-Leak geschaffen hast.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Nimral »

Socke hat geschrieben:
Fr 21. Jan 2022, 23:43
Das Exception-Handling funktioniert wie immer mit try..finally und try..except. Es gibt nur einen kleinen Unterschied: Führt die Exception dazu, dass die Execute-Methode verlassen wird, wird das Exception-Objekt in der Eigenschaft FatalException abgelegt und kann dann dort abgefragt werden.
Das ist gut zu wissen, ich denke bei Gelegenheit drüber nach was ich dagegen mache. Ich habe aber Execute sowieso über einen äußeren try ... except Block gesichert, also kann eigentlich keine Exception aus dem execute entkommen. Oder gibt es da irgendwelche bekannten seltsamen Ausnahmen?

Ich baue das Konzept von oben übers WE ein, und dann kommt es sowieso auf die Folterbank (PenTest und Wanem mit massiven Paketverlusten). Mal sehen ob es hält was es verspricht :-)

Danke euch mal wieder!

Ich denke auch, das Beispiel eignet sich gut als Demo fürs Wiki, ich bau da ein Tutorial draus.

Armin.
Zuletzt geändert von Nimral am Sa 22. Jan 2022, 07:55, insgesamt 2-mal geändert.

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Nimral »

Winni hat geschrieben:
Fr 21. Jan 2022, 21:26
FreeOnTerminate passiert dann nie, wenn Du keinen TimeOut definiert hast.
Hab ich schon auf der ToDo Liste. Kann ich aber noch nichts dazu sagen, das Verhalten der Socket-Schnittstellen beider Zielplattformen für alle denkbaren gruseligen Netzwerkanomalitäten ist [mir] weitgehend unbekannt. Mal sehen ob lNet robust genug ist. Kümmer ich mich drum wenn das Programm auf der Folterbank in diesem Punkt versagen sollte.

Der derzeitige Testcode macht bereits was in der Richtung, alle Reads haben einen Timeout, und wenn ein Task paar Sekunden nicht mehr bedient wird mache ich die Connection von mir aus zu. Da der Client REST ist, also keine langen Sessions macht, denke ich, dass ich damit durchkomme.

Außerdem wird er von einem Kollegen entwickelt -- ich kann also notfalls, was Luxus, auch den Source-Code des Clients einsehen.

Armin.

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von af0815 »

schon gesehen ?

viewtopic.php?t=12783

in den letzten beiden Posts werden Hinweise genannt, wo man sich fertigen Code anschauen kann, der auch Kommunikation betreibt. Nur als Ergänzung.
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Nimral »

Socke hat geschrieben:
Fr 21. Jan 2022, 20:39
Hier brauchst du keine CriticalSection. InterlockedIncrement ist da AFAIK schneller.
Hab ich kurz angesehen, passt m.E. nicht. Ich muss ja nach meinem Konzept das Tauschen der TFPGObjectList<T> mit der selben Criticalsection sichern --> da InterlockedIncrement, das intern vermutlich auch eine CriticalSection benützt, diese für sich behält, brauch ich meine Eigene.

Armin.

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Nimral »

Ich glaube, ich hab ein Problem gefunden. Ausgangssituation: der Reader-Task wartet auf CanUpdate. Der letzte Netzwerk-Task kommt zum Ende, Counter wird 0, CanUpdate feuert, der Reader-Task läuft los. Genau in diesem Moment läuft wieder ein Netzwerk-Task ein. Jetzt habe ich eine Race-Condition um die CriticalSection. So lange der Reader-Task gewinnt, ist alles gut. Gewinnt der Netzwerk-Task, setzt er den Counter hoch und rennt weiter, die CriticalSection wird freigegeben, der Reader-Task rennt los und zieht dem Netzwerk-Task die TFPGObjectList unter dem Hintern weg.

Die Chance dass das passiert ist natürlich denkbar gering, ich könnte mich notfalls auf das Try/Catch der Netzwerk-Tasks verlassen, geht halt mal eine Abfrage schief, der Client wirds ja vermutlich gleich nochmal probieren,. Gibt es nicht irgendwo ein theoretisches Konzept wie man so ein Locking unter FPC aufbaut welches alle denkbaren Race-Conditions abbügelt? Gibt es das überhaupt theoretisch?

Derzeit wäre meine Idee, in der CriticalSection im Reader-Task nochmal den Counter zu prüfen. Ist er nicht mehr 0, den Reader-Task abbrechen und in eine weitere Testschleife schicken. Kommt das Daten-Update halt etwas später.

Armin.

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von Socke »

Nimral hat geschrieben:
Sa 22. Jan 2022, 08:53
Ich glaube, ich hab ein Problem gefunden. Ausgangssituation: der Reader-Task wartet auf CanUpdate. Der letzte Netzwerk-Task kommt zum Ende, Counter wird 0, CanUpdate feuert, der Reader-Task läuft los. Genau in diesem Moment läuft wieder ein Netzwerk-Task ein. Jetzt habe ich eine Race-Condition um die CriticalSection. So lange der Reader-Task gewinnt, ist alles gut. Gewinnt der Netzwerk-Task, setzt er den Counter hoch und rennt weiter, die CriticalSection wird freigegeben, der Reader-Task rennt los und zieht dem Netzwerk-Task die TFPGObjectList unter dem Hintern weg.
Dazu hast du doch die CriticalSection. Solange dein Reader-Task diese hält, muss der Netzwerk-Task warten und anders herum. Das Betriebssystem sorgt schon dafür, dass immer nur ein Thread die CriticalSection erhält und gestartet wird. Der Netzwerk-Task muss sie dann selbstverständlich solange halten, wie er irgendwelche Operationen mit der Liste durchführt. Die Zeit kannst du verkürzen, indem du z.B. eine Kopie anlegst.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Tasks Synchronisieren mit Event und CriticalSection

Beitrag von af0815 »

Kann der Netzwerktask seine Daten aus der grossen Liste die nicht umbuchen in seine Threadvariablen ? Das natürlich in der Critical section, oder muss der Netzwerktask daten zurückliefern ?
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Antworten