Multitasking: threadsicherer Zugriff auf einen String-Puffer

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

Multitasking: threadsicherer Zugriff auf einen String-Puffer

Beitrag von Nimral »

Fast ist es geschafft. Die neue Version meines Messprogramms nimmt Gestalt an. Jetzt brauche ich noch einen kleinen Tipp, den gemeinsamen Zugriff zu organisieren.

Im Kern werkelt eine Routine, die eine serielle Schnittstelle mit Daten füttert. In der anderen Richtung laufen serielle Daten ein und werden an einen Stringpuffer angehängt. Sowohl beim Senden als auch beim Empfangen werden die einzelnen Datenpakete mit <CR> getrennt. Der Code im Zentrum sieht in etwa so aus (Auszug - der richtige Code ist wesentlich länger da er mehrere inienander verschränkte Datenpfade verwalten muss, Beispiel ohne Test runtergetippt, aber mir gehts nur ums Prinzip und nicht um die Details):

Code: Alles auswählen

 
procedure TSerialThread.execute;
 
begin
while not terminated do
   begin
   // Wenns was zu senden gibt, raus damit.
   if pos(CR,SendBuffer) > 0 then
      begin
      ComSend(LeftOfCR(SendBuffer));
      SendBuffer := RightOfCR(SendBuffer);
      end;
   // Empfangsversuch, 200ms lang alle einlaufenden zeichen bündeln.
   // CR ist bereits in den Eingangsdaten enthalten
   ReceiveBuffer := ReceiveBuffer + ComRead(200);
   end;
end;
 
procedure TSerialThread.Send(S:String);
 
// Hier kippen andere Threads die zu sendenden Daten ab
 
 begin
   SendBuffer := SendBuffer + CR + S;
end;
 
function TSerialThread.Receive : String;
 
// hier holen andere Threads empfangene Daten ab,
// immer ein cr getrenntes Paket auf einmal, "" wenn der
// Puffer leer ist
 
begin
   result := '';
   if pos(CR,ReceiveBuffer) > 0 then
      begin
      result := LeftOfCR(ReceiveBuffer);
      ReceiveBuffer := RightOfCR(ReceiveBuffer);
      end;
end;
 


Send und Receive werden von mindestens einem anderen Thread aufgerufen. Es ist sichergestellt, dass mindestens ein Thread (die Anzeige-GUI) am Receive hängt, so dass der Empfangspuffer nicht überlaufen kann.

Sodala. Jetzt habe ich einige Stunden und Notizblätter lang versucht, mit Nachdenken draufzukommen, ob der Code in sich Thread-Safe ist, oder ob ich CriticalSections verteilen muss. Ich fürchte, dass Strings beim gemeinsamen Zugriff tückisch sind, und der Code ist es schon zweimal, weil sowohl die Sende - als auch die Empfangsroutine gleicheitig an den Puffern herumwerkeln.

Einfach auf "Verdacht" überall Critical markieren und dann mit Testen sehen ob ich es getroffen habe möchte ich des Lernens wegen nicht, außerdem soll die Execute Methode andere Threads möglichst wenig am Arbeiten hindern.

Wer hilft mir, den Thread-Code sicher zu machen und nimmt sich auch noch die Zeit, ein paar Zeilen dazuzuschreiben, warum er was gerade wo macht, und warum nicht? Ab dem nächsten Projekt würde ich es gerne selber gebacken bekommen.

Herzlichen Dank,

Armin.

Benutzeravatar
willi4willi
Lazarusforum e. V.
Beiträge: 155
Registriert: Sa 1. Nov 2008, 18:06
OS, Lazarus, FPC: Windows, Linux (debian) / Lazarus 3.2 / FPC 3.2.2
CPU-Target: i386, win64, arm

Re: Multitasking: threadsicherer Zugriff auf einen String-Pu

Beitrag von willi4willi »

Hallo Armin,

wenn du es so machst, wie beschrieben, dann würde ich vor den Füllen des Buffers eine boolsche Variable setzen, die anschließend wieder zurückgesetzt wird.
Wenn der Buffer ausgelesen werden soll, dann sollte diese Variable nicht gesetzt sein, dann dann wird ja gerade was reingeschrieben.

Gibt es eigentlich einen Grund, warum du bei einkommenden Daten keinen Event auslöst?
 

Viele Grüße

Willi4Willi

------------

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

Re: Multitasking: threadsicherer Zugriff auf einen String-Pu

Beitrag von Nimral »

Guten Abend, und danke für Deine schnelle Antwort!

willi4willi hat geschrieben:Hallo Armin,

wenn du es so machst, wie beschrieben, dann würde ich vor den Füllen des Buffers eine boolsche Variable setzen, die anschließend wieder zurückgesetzt wird.
Wenn der Buffer ausgelesen werden soll, dann sollte diese Variable nicht gesetzt sein, dann dann wird ja gerade was reingeschrieben.

Gibt es eigentlich einen Grund, warum du bei einkommenden Daten keinen Event auslöst?


Ich machs tatsächlich mit einem Event. Der Teil ist der Verkürzung des Beispielcodes zum Opfer gefallen, weil er m.E. nichts mit dem gemeinsamen Zugriff zu tun hat, und weil es noch x andere Events (Fehlerbedingungen) gibt. Ich löse einen Event aus, so lange im ReceiveBuffer mindestens ein CR gefunden wird.

Code: Alles auswählen

 
   // Empfangsversuch, 200ms lang alle einlaufenden zeichen bündeln.
   // CR ist bereits in den Eingangsdaten enthalten
   ReceiveBuffer := ReceiveBuffer + ComRead(200);
   if pos(cr,ReceiveBuffer) > 0 then CommandReceived.SetEvent;
   ...
 


... und die Lese -Threads arbeiten prinzipiell so

Code: Alles auswählen

 
...
    if TSerialThread.CommandReceived.WaitFor(5000) = wrSignaled then
       Value := TSerialThread.Receive;
    else
      // irgendeine Fehlerbehandlung --> Timeout, das Peripheriegerät sendet nix mehr
 


... aber das ist m.E. eine andere Baustelle. Damit kann ein potenzieller Empfänger warten bis es sich lohnt, überhaupt receive aufzurufen. Ich denke, das habe ich richtig gemacht. Mir gehts nur um die beiden Buffer in der Empfänger-Routine, da auf diese gemeinsam zugegriffen wird.

Ich habe jetzt auch ehrlich eine halbe Stunde über eine boolean Variable nachgedacht, eigentlich sinds ja zwei, eine für jeden Buffer? Wie genau sollen die mich retten? Da execute, send und receive im gleichen Thread laufen, und ich ja irgendwie darauf warten müsste, dass die Variable freigegeben wird, würde ich den Thread in einen Deadlock schicken:

Code: Alles auswählen

 
... in execute:
   Receiving := true;
   // Empfangsversuch, 200ms lang alle einlaufenden zeichen bündeln.
   // CR ist bereits in den Eingangsdaten enthalten
   ReceiveBuffer := ReceiveBuffer + ComRead(200);
   if pos(cr,ReceiveBuffer) > 0 then CommandReceived.SetEvent;
   Receiving := false;
 
... in receive
 
function TSerialThread.Receive : String;
 
// hier holen andere Threads empfangene Daten ab,
// immer ein cr getrenntes Paket auf einmal, "" wenn der
// Puffer leer ist
 
begin
   result := '';
   while Receiving do ;
   if pos(CR,ReceiveBuffer) > 0 then
      begin
      result := LeftOfCR(ReceiveBuffer);
      ReceiveBuffer := RightOfCR(ReceiveBuffer);
      end;
end;
 


Sobald Receive zufällig innerhalb des Empfangsfensters aufgerufen würde, würde die while Schleife den kompletten TSerialThread zum Stehen bringen. Ich müsste den Code von Receive irgendwie aus dem Thread auskoppeln, oder gruselige Ansätze wie Delays verteilen versuchen, damit execute irgendwann weiter läuft. Und würde mir sofort die nächste Ohrfeige einfangen: während receive seine Artbeit macht könnte execute die nächste Loop durchlaufen und receive einholen, also nochmal eine Variable, mit der execute am Empfängerteil vorbeigelotst wird bis receive wieder verlassen wurde? Klingt für mich vom Gefühl her wie eine Einladung zum Deadlock.

Ich denke, ich muss das auf Thread-Ebene lösen. Immer wenn execute so einen send/receive Zyklus durchlaufen hat, also am Anfang oder am Ende der while not terminated Schleife, müssen die anderen Threads ihre Arbeit machen. Ich dachte, das sei der Sinn von CriticalSection: ich würde eine um den Sender und eine um den Empfänger-Block in execute schreiben, so lange sich der Code von execute innerhalb eines dieser beiden Blöcke befindet haben die anderen Threads Pause. Und in Send und Receive ebenso, damit der execute Thread vor ihren Aktivitäten geschützt wird.

So irgendwie:

Code: Alles auswählen

 
procedure TSerialThread.execute;
 
begin
while not terminated do
   begin
   // Wenns was zu senden gibt, raus damit.
   if pos(CR,SendBuffer) > 0 then
      begin
      EnterCriticalSection(CritSect);
      ComSend(LeftOfCR(SendBuffer));
      SendBuffer := RightOfCR(SendBuffer);
      LeaveCriticalSection(CritSect);
      end;
   // Empfangsversuch, 200ms lang alle einlaufenden zeichen bündeln.
   // CR ist bereits in den Eingangsdaten enthalten
   S := ComRead(200);
   if S <> '' then
      begin
      EnterCriticalSection(CritSect);
      ReceiveBuffer := ReceiveBuffer + S;
      LeaveCriticalSection(CritSect);
   end;
end;
 
procedure TSerialThread.Send(S:String);
 
// Hier kippen andere Threads die zu sendenden Daten ab
 
 begin
      EnterCriticalSection(CritSect);
    SendBuffer := SendBuffer + CR + S;
      LeaveCriticalSection(CritSect);
end;
 
function TSerialThread.Receive : String;
 
// hier holen andere Threads empfangene Daten ab,
// immer ein cr getrenntes Paket auf einmal, "" wenn der
// Puffer leer ist
 
begin
   result := '';
   if pos(CR,ReceiveBuffer) > 0 then
      begin
      EnterCriticalSection(CritSect);
      result := LeftOfCR(ReceiveBuffer);
      ReceiveBuffer := RightOfCR(ReceiveBuffer);
      LeaveCriticalSection(CritSect);
      end;
end;
 


Das basiert jetzt aber mehr auf meinem Wunschdenken bzw. einer vagen Idee wie mit CriticalSection umzugehen ist. Wem "gehört" eigentlich der Codfe von TSerialThread.Send bzw. TSerialThread.Receive? Dem SerialThread, wo die Routinen drinnen stehen, oder dem Thread der die beiden Routinen gerade aufgerufen hat? Wäre es so, dann würde die Geschichte mit der Variablen wieder irgendwie plausibler.

Armin.

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Multitasking: threadsicherer Zugriff auf einen String-Pu

Beitrag von Warf »

willi4willi hat geschrieben:wenn du es so machst, wie beschrieben, dann würde ich vor den Füllen des Buffers eine boolsche Variable setzen, die anschließend wieder zurückgesetzt wird.


Auf gar keinen Fall, das ist nicht Threadsicher. In der zeit zwischen dem auslesen und dem Setzen der Variable kann der Thread unterbrochen werden und Zack du hast zwei threads die gemeinsam auf die Variable zugreifen.

Lösung: InterlockedCompareExchange. InterlockedCompareExchange liest einen wert aus und setzt ihn atomar, d.h. das es keine möglichkeit gibt das ein zweiter thread sich dazwischen mogeln kann, Beispiel:

Code: Alles auswählen

var lock: Cardinal;
...
if InterlockedCompareExchange(lock, 1, 0) = 0 then
try
  // Lock bekommen
finally
  lock := 0;
end
else
begin
 // Aktuell von nem anderen Thread gelockt
end;


Was man aber wahrscheinlich machen will sind Critical sections, die machen das selbe, nur warten (nicht busy) darauf das das lock freigegeben wird:

Code: Alles auswählen

var lock: RTLCriticalSection;
...
EnterCriticalSection(lock);
try
 
finally
  LeaveCriticalSection(lock);
end;


Beispiel, Als property:

Code: Alles auswählen

TMyThread = class(TThread)
private
  FStringLock: TRTLCriticalSection;
  FStringData: String;
 
  function GetStringData: String;
  procedure SetStringData(const AValue: String);
public
  constructor Create(...);
  destructor Destroy; override;
 
  property StringData: String read GetStringData write SetStringData;
end;
 
...
 
function TMyThread.GetStringData: String;
begin
  EnterCriticalSection(FStringLock);
  try
    Result := FStringData;
    // Garantiert eine neue Kopie des strings
    SetLength(Result, Result.Length);
  finally
    LeaveCriticalSection(FStringLock);
  end;
end;
 
procedure TMyThread.SetStringData(const AValue: String);
begin
  EnterCriticalSection(FStringLock);
  try
    FStringData := AValue;
    // Garantiert eine neue Kopie des strings
    SetLength(FStringData, FStringData.Length);
  finally
    LeaveCriticalSection(FStringLock);
  end;
end;
 
constructor TMyThread.Create(...);
begin
 ...
  InitCriticalSection(FStringLock);
end;
 
destructor TMyThread.Destroy;
begin
 ...
  DoneCriticalSection(FStringLock);
end;


Solang man auf StringData zugreift und nicht auf FStringData direkt, ist man sicher

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

Re: Multitasking: threadsicherer Zugriff auf einen String-Pu

Beitrag von Nimral »

Hi Warf,

mein Beispiel von oben (GoTo) ist also prinzipiell richtig? Ich habe zwar keine Getter und Setter geschrieben, da die StringBuffer ja nicht nach außen sichtbar werden, aber das Prinzip der CriticalSections wohl halbwegs richtig erraten?

3 Fragen hätte ich noch :-)

1.) In meinem Beispiel ... würde es irgend etwas verbessern, wenn ich zwei CriticalSections mache, eine für den Zugriff auf den Sende- und eine für den Zugriff auf den Empfangspuffer? Ich meine: ja, die Chance dass ein aufrufender Thread in eine gelockte CriticalSection läuft und für nix warten muss weil der Lock gerade den anderen Puffer schützt wird geringer.

2.) Macht es Sinn, und gibt es ein spezielles Kommando um dem Thread-Scheduler unter die Arme zu greifen, wenn der Code in Execute sich gerade *nicht* innerhalb einer CriticalSection befindet? Ich kenne Systeme die Befehle wie "Yield" einführen, um sowas zu erreichen:

Code: Alles auswählen

 
while not terminated do
   begin
   EnterCriticalSection(FStringLock);
   ... was auch immer ...
   LeaveCriticalSection(FStringLock);
   Yield();
   EnterCriticalSection(FStringLock);
   ... was auch immer ...
   LeaveCriticalSection(FStringLock);
   Yield();
   end;
 


Gibt es FPC sowas, macht es SInn, es zu verwenden?

3.) Der serielle Empfänger-Thread hat auf einer schnellen Maschine, an der nur wenige Messgeräte angeschlossen sind, vergleichsweise selten etwas zu tun, meistens läuft die while not terminated Schleife leer durch, weil es nix zu senden oder zu empfangen gibt. Macht es Sinn, den Thread irgendwie zu bremsen, z.B. durch Einfügen von Sleep() Kommandos, oder öfters mal Zeitschleifen abzugeben, damit andere Threads mehr machen können? Ich habe versucht, das mit der Zeiole

Code: Alles auswählen

 
S := ComRead(200);
 


zu erreichen. Das betroffene Messgerät sendet 4 Mal pro Sekunde (alle 250ms), ein Datenpaket ist etwa 150ms lang, ich dachte dass ich mit diesem Delay eine Chance habe, dass sich der Empfänger-Thread mit dem Messgerät synchronisiert, dass also möglichst wenig Messwerte auf zwei ComReads verteilt werden. Ich bin da auch auf ein Kommando

Code: Alles auswählen

 
ThreadSwitch();
 


gestoßen. Würde das auch Frage (3) beantworten? Was natürlich bei meiner konkreten Anwendung keinen rechten Sinn macht weil alle anderen Threads, wenn kein Messwert eingelaufen ist, auch nichts zu tun haben, es gibt ja nichts anzuzeigen oder zu protokollieren, aber eventuell findet die Maschine dann öfter in den Idle Status und spart Strom? Vielleicht ist das Multithreading (WIndows & Linux) ja auch so schlau dass dann nicht nur Threads des eigenen Prozesses unterstützt werden, sondern sogar Threads anderer Prozesse?

HG, Armin.

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: Multitasking: threadsicherer Zugriff auf einen String-Pu

Beitrag von mschnell »

Nimral hat geschrieben:Vielleicht ist das Multithreading (WIndows & Linux) ja auch so schlau dass dann nicht nur Threads des eigenen Prozesses unterstützt werden, sondern sogar Threads anderer Prozesse?

Vielleicht ist es niocht so schlau einen String zu nehmen.
Mit einer TTheradList von Transfer-Objekten (siehe Deine anderen Diskussionen) sollte es kein Problem sein.
-Michael

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Multitasking: threadsicherer Zugriff auf einen String-Pu

Beitrag von Warf »

Nimral hat geschrieben:Ich habe zwar keine Getter und Setter geschrieben, da die StringBuffer ja nicht nach außen sichtbar werden

Ich würd trozdem nen getter und setter machen, und halt ne private property (muss ja nicht public sein). Damit kannst du nicht ausversehen mal den string direkt benutzen, weil du die CS vergessen hast. Der inliner wird wahrscheinlich bei der Optimierung die funktionsaufrufe wegmachen, sodass du keine performance verlierst.

Nimral hat geschrieben:1.) In meinem Beispiel ... würde es irgend etwas verbessern, wenn ich zwei CriticalSections mache, eine für den Zugriff auf den Sende- und eine für den Zugriff auf den Empfangspuffer? Ich meine: ja, die Chance dass ein aufrufender Thread in eine gelockte CriticalSection läuft und für nix warten muss weil der Lock gerade den anderen Puffer schützt wird geringer.

Genau, eine Critical Section für jede resource die nur allein zugegriffen werden darf. Aber aufgepasst, sobald du zwei resourcen hast kanns zum deadlock kommen, wenn Thread1 resource 1 alloziiert, Thread 2 resource 2, und dann thread1 auf resource 2 wartet und thread2 auf resource 1. Keiner kann fertig werden da er auf den anderen wartet. Daher locks immer sehr klein halten, darum hatte ich die getter und setter geschrieben die eine Kopie des Strings erzeugen, somit kann der string außerhalb der CS nicht mehr verändert werden und man muss keine komplizierten locks haben die eventuell zu groß werden.

Nimral hat geschrieben:2.) Macht es Sinn, und gibt es ein spezielles Kommando um dem Thread-Scheduler unter die Arme zu greifen, wenn der Code in Execute sich gerade *nicht* innerhalb einer CriticalSection befindet? Ich kenne Systeme die Befehle wie "Yield" einführen, um sowas zu erreichen:

Gibt es FPC sowas, macht es SInn, es zu verwenden?

Ich weiß nicht ob es yield im fpc gibt (wahrscheinlich, schon, ich kenns nur nicht), aber sleep(0) tut das selbe.

Nimral hat geschrieben:3.) Der serielle Empfänger-Thread hat auf einer schnellen Maschine, an der nur wenige Messgeräte angeschlossen sind, vergleichsweise selten etwas zu tun, meistens läuft die while not terminated Schleife leer durch, weil es nix zu senden oder zu empfangen gibt. Macht es Sinn, den Thread irgendwie zu bremsen, z.B. durch Einfügen von Sleep() Kommandos, oder öfters mal Zeitschleifen abzugeben, damit andere Threads mehr machen können? Ich habe versucht, das mit der Zeiole

zu erreichen. Das betroffene Messgerät sendet 4 Mal pro Sekunde (alle 250ms), ein Datenpaket ist etwa 150ms lang, ich dachte dass ich mit diesem Delay eine Chance habe, dass sich der Empfänger-Thread mit dem Messgerät synchronisiert, dass also möglichst wenig Messwerte auf zwei ComReads verteilt werden. Ich bin da auch auf ein Kommando

Code: Alles auswählen

 
ThreadSwitch();
 


gestoßen. Würde das auch Frage (3) beantworten? Was natürlich bei meiner konkreten Anwendung keinen rechten Sinn macht weil alle anderen Threads, wenn kein Messwert eingelaufen ist, auch nichts zu tun haben, es gibt ja nichts anzuzeigen oder zu protokollieren, aber eventuell findet die Maschine dann öfter in den Idle Status und spart Strom? Vielleicht ist das Multithreading (WIndows & Linux) ja auch so schlau dass dann nicht nur Threads des eigenen Prozesses unterstützt werden, sondern sogar Threads anderer Prozesse?

HG, Armin.

Wenn du den prozess freigibst (yield unter java, sleep(0), etc.) sagt dem Betriebsystem: "dieser thread hat grad nix zu tun, tu doch nen anderen thread die CPU" und der thread wird damit ganz normal wieder gescheduled. Wie genau das scheduling mit threads abläuft weiß ich nicht, aber ich glaube threads werden vom betriebsystem selbst gescheduled, d.h. wenn du deinen thread yieldest, entscheidet das OS welcher thread als nächstes geladen wird, der kann von deinem Programm oder von einem anderen sein. So wie das OS entscheidet das es am fairsten ist. Darüber würde ich mir keine gedanken machen. Wenn du nix zu tun hast solltest du aber auf jeden fall yielden und kein busy waiting machen

Antworten