Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   Delphi Synchronisieren von hauptthread und Teilthread (https://www.delphipraxis.net/109843-synchronisieren-von-hauptthread-und-teilthread.html)

zorro08 8. Mär 2008 22:37


Synchronisieren von hauptthread und Teilthread
 
Hallo da draußen !

In meinem Programm scheinen RTEs daher zu kommen, dass ein bestimmter Thread, nennen wir ihn Thread_A, bestimmte Daten verändert und der Hauptthread, z.B. in einem TimerEvent oder einem form-Refresh manchmal genau in dem Moment auf diese Daten zugreift, wenn es offensichtlich nicht passt.

Ich habe versucht, den Code in der Timerroutine bzw. beim Aufbau der Anzeige des Hauptthreads mit CriticalSection.enter / Leave solange auszusperren, bis der thread_a mit der Berechnung fertig ist.

Aber dies klappt offensichtlich nicht. Der Code im Hauptthread hinter dem "cs.enter" wurde immer ausgeführt. Darin besteht mein Problem.

Weitere Erkentnisse in diesem Zusammenhang:

o Im Hauptthread kann man mehrmals dasselbe criticalSection.enter aufrufen.
o Der thread_a bleibt beim ersten cs.enter stehen und läuft beim nächsten cs.Leave aus dem Hauptthread wieder weiter.
o N x cs.enter erfordern auch N x cs.leave, damit thread_a wieder läuft
o Es gibt einen Totalstillstand, wenn zuerst cs.leave ohne vorheriges cs.enter aufgerufen wird

Vielen Dank vorab für einen Hinweis in dieser Sache.

Dani 8. Mär 2008 23:07

Re: Synchronisieren von hauptthread und Teilthread
 
Abend!
  • Menge der Daten, auf die sowohl Thread_A als auch der Hauptthread zugreifen, minimieren.
  • Critical sections nur im Hauptthread verwenden:
    Delphi-Quellcode:
    procedure TForm1.Timer1Timer(Sender: TObject);
    begin
      CS.Enter;
      try
        Label1.Caption := SomeDataToString(Thread_A.SomeData);
      finally
        CS.Leave;
      end;
    end;
  • Lieber dem Thread ein Ereignis geben, welches mit Synchronize() aufgerufen wird.
    Delphi-Quellcode:
    type
      TSyncEvent = procedure(Sender: TObject{; ....}) of object;

      TThread_A = class(TThread)
      private
        FOnSync: TSyncEvent;
        procedure DoOnSync;
      public
        property OnSync TSyncEvent read FOnSync write FOnSync;
      end;

    implementation

    procedure TThread_A.DoOnSync;
    begin
      Assert(Assigned(FOnSync));
      FOnSync(Self);
    end;

    procedure TThread_A.Foo;
    begin
      if Assigned(FOnSync) then Synchronize(DoOnSync);
    end;
  • Den Datenaustausch zwischen Hauptthread und Thread_A in eine Containerklasse auslagern, welche mit einem Singleton des TMultiReadExclusiveWriteSynchronizer den Zugriff regelt.

zorro08 9. Mär 2008 11:28

Re: Synchronisieren von hauptthread und Teilthread
 
Danke für die extrem schnelle Antwort (5 min !, und dass um 24:00)
Ich habe die Anregungen ausgewertet und z.T. sofort getestet, aber sie lösen mein Problem wahrscheinlich nicht:

Zitat:

Menge der Daten, auf die sowohl Thread_A als auch der Hauptthread zugreifen, minimieren.
Aus bestimmten Gründen nur mit großem Aufwand möglich, aber der neue Mechanismus soll ja auch mit großen Datenmengen funktionieren


Zitat:

Critical sections nur im Hauptthread verwenden:Delphi-Quellcode: markieren
procedure TForm1.Timer1Timer(Sender: TObject);
begin
CS.Enter;
try
Label1.Caption := SomeDataToString(Thread_A.SomeData);
finally
CS.Leave;
end;
end;
Das scheint gerade das Problem zu sein: cs.enter / cs.leave klappen bei mir im Hauptthread eben insofern nicht, dass der Teil nach dem cs.enter immer ausgeführt wird. auch wenn sich thread_a innerhalb der cs befindet und darin die Daten gerade beschrieben werden.

Vielleicht ist es relevant, wie das beschreiben abläuft: Es wird im thread_a in der CS eine Sensorik angesprochen und deren Meßwerte u.a. in eine ASC-Datei geschrieben

Der haupthtread soll dann z.B. in der Timerroutine oder auf BtnClick diese ASC-Datei innerhalb einer CS wieder parsen und die Daten grafisch anzeigen. Da stört es natürlich, wenn thread_a das File gerade schreibt und der Hauptthread schon wieder darauf zugreifen will.

Dies hat komischerweise auch eine Weile geklappt. Neuerdings treten aber totale blockaden auf, die ich von früher nicht kannte.



Zitat:

Lieber dem Thread ein Ereignis geben, welches mit Synchronize() aufgerufen wird.
Delphi-Quellcode: zusammenfalten | markieren
1


Ich würde heute die ganze Sache anders anpacken, aber es handelt sich um alten Code, der noch ein bischen halten soll. Wenn cs.enter / cs.leave klappen würde, wäre ja alles gut.


Zitat:

Den Datenaustausch zwischen Hauptthread und Thread_A in eine Containerklasse auslagern, welche mit einem Singleton des TMultiReadExclusiveWriteSynchronizer den Zugriff regelt.

Habe ich getestet, verhält sich aber in Bezug auf Hautpthread / Thread wie cs.enter/cs.leave, geht also nicht: Ich kann im Hauptthread mehrmals MultiReadExclusiveWriteSynchronizer.BeginWrite aufrufen, wobei der Code dahinter immer abgearbeitet wird.

Ich versuche jetzt mal WaitForMultipleObjects als Warte - Mechanismus.

Für Hinweise bin ich weiterhin dankbar
Tom.

zorro08 9. Mär 2008 18:03

Re: Synchronisieren von hauptthread und Teilthread
 
Nach einigen Stunden Beschäftigung mit dem Thema sieht die Situation so aus:

1. Das Sperren des Hauptthreads klappt mit WaitForSingleObject(Event_0,INFINITE). Und zwar so gut, dass er bei falscher Anwendung niemals wieder ans Laufen kommt.

Falsche Anwendung bedeutet:

a) Entweder wurde nicht organisiert, dass mindestens ein Event, z.B. von einem Thread, kommt.
b) Oder mitten in die TimerRoutine platzt dann ein BtnClickEvent, der ebenfalls WaitForSingleObject(Event_0,INFINITE) enthält und damit seinerseits auf einen Event wartet, der später eigentlich von der Timerroutine ausgelöst werden sollte, um die Lock/Unlock-Kette am laufen zu halten.

Klassischer Deadlock !

Die (für mich) richtige Anwendung wäre:

o Es wird ein Vorratsevent direkt nach dem Erzeugen des Events aktiviert

Delphi-Quellcode:
event0        := CreateEvent(nil, False, False, nil);
SetEvent(Event0);
o Dann kommen die Threads oder der hauptthread mittes WaitForSingleObject( Event_0, 10 ) irgenwann zufällig zum Zuge
o nach der Datenbearbeitung oder - Nutzung im Thread/HauptThread wird direkt wieder ein Event aktiviert

Delphi-Quellcode:
//----------------------------------------------
procedure TForm1.Timer1Timer(Sender: TObject);
//----------------------------------------------
var wfmo: DWORD;
    s: string;
    tp2: int64;
    ccc: integer;
begin

  Timer1.Enabled:=FALSE;

  if fn_GetTimerSec(tp1) > 4.0 then
  begin
    if (WaitForSingleObject(Event0,10) = WAIT_OBJECT_0) then
    begin

      pr_StartTimer(tp2);
      ccc:=0;
      repeat
        s1:= 'mainThread 2 Sekunden Arbeit '+__s(ccc);
        label1.Caption := s1;
        label1.refresh;
        inc(ccc);
      until fn_GetTimerSec(tp2) > 2.0;

      pr_StartTimer(tp1);
      s1:= 'mainThread idle';
      SetEvent(Event0);
    end;

  end;

  inc(cc);
  label1.Caption := s1 + IntToStr(cc)+'               ';
  label1.refresh;

  Timer1.Enabled:=TRUE;
end;
bzw.

Delphi-Quellcode:
//----------------------------------
procedure TThread_a.Execute;
//----------------------------------
var tp2,tp1: Int64;
    ccc: Integer;
begin
  inherited;
  pr_StartTimer(tp1);
  s1:= 'Thread_a idle ';
  cc:=0;
  pr_StartTimer(tp1);
  while TRUE do
  begin
    sleep(1);

    if Terminated then
    begin
      exit
    end

    if fn_GetTimerSec(tp1) > 4.0 then
    begin
      if WaitForSingleObject(Event0,10) = WAIT_OBJECT_0 then
      begin
        ccc:=0;
        pr_StartTimer(tp2);
        repeat
          s1:= 'Thread_a Event0, 2 Sekunden Arbeit ! '+__s(ccc);
          sleep(1);
          inc(ccc);
        until fn_GetTimerSec(tp2) > 2;
        s1:= 'Thread_a idle ';
        show;
        pr_StartTimer(tp1);
        SetEvent(Event0);
      end;
    end;
    inc(cc);
  end
end;
o Damit kommem alle im richtigen Moment mal dran, insbesondere eben auch der Hauptthread


Meine Abhilfe sieht jetzt so aus:

o Die Teilthreads, aber nicht der hauptthread, werden weiterhin mit CriticalSection voreinander geschützt
o Der HauptThread stellt seine Auswerte/Anzeigeroutine dem Thread per EreignisProzedur zur Verfügung

Delphi-Quellcode:
  auftObj.thread_messen.onMeasThreadProc := pr_ThreadProc;
o Der Thread führt diese Routine per Synchronize() aus.

Delphi-Quellcode:
//----------------------------------------------------------------
procedure Tthread_messen.pr_SyncProc;
//----------------------------------------------------------------
begin
  onMeasThreadProc(self)
end;
Delphi-Quellcode:
   if assigned(onMeasThreadProc) then
    begin
      Synchronize(pr_SyncProc);
    end;
o Während der Ausführung kann kein Repaint oder Neuaufbau der Anzeigen erfolgen, da die Ereignisprozedur kein KeepWindowsAlive oder Sleep() enthält.
o Erst wenn die Auswerte/Anzeigeroutine komplett am Stück beendet ist, kommt der HauptThread zum Zuge. Dann ist alles Kritische aber schon erledigt.

Für weitere erhellende Kommentare bin ich selbstverständlich dankbar. Ansonsten scheint mein spezielles Problem durch diese Verfahrensweise gelöst zu sein.
Danke nochmals an Dani für die schnelle Reaktion.
Klasse Forum, klasse Seite !

Apollonius 9. Mär 2008 19:41

Re: Synchronisieren von hauptthread und Teilthread
 
Nur ganz am Rande:
Zitat:

Delphi-Quellcode:
event0        := CreateEvent(nil, False, False, nil);
SetEvent(Event0);

In CreateEvent kann man auch gleich angeben, dass das Event gesetzt werden soll.

sirius 10. Mär 2008 08:41

Re: Synchronisieren von hauptthread und Teilthread
 
Zitat:

Zitat von zorro08
Ich habe versucht, den Code in der Timerroutine bzw. beim Aufbau der Anzeige des Hauptthreads mit CriticalSection.enter / Leave solange auszusperren, bis der thread_a mit der Berechnung fertig ist.

Ist (neben dem Syncronize aus der VCL) die einfachste Variante und sollte zuverlässig funktionieren, solange man überall dasselbe Handle verwendet.
Zitat:

Zitat von zorro08
Aber dies klappt offensichtlich nicht. Der Code im Hauptthread hinter dem "cs.enter" wurde immer ausgeführt. Darin besteht mein Problem.

:gruebel: hmmm, da hst du etwas falsch gemacht.

Zitat:

Zitat von zorro08
o Im Hauptthread kann man mehrmals dasselbe criticalSection.enter aufrufen.

Jep, da wird nur ein Zähler hochgezählt, deswegen immer in einen finally-Block um das "leave" setzen.
Zitat:

Zitat von zorro08
o Der thread_a bleibt beim ersten cs.enter stehen und läuft beim nächsten cs.Leave aus dem Hauptthread wieder weiter.

Wenn der Zähler wieder bei 0 ist.
Zitat:

Zitat von zorro08
o N x cs.enter erfordern auch N x cs.leave, damit thread_a wieder läuft

Ja, Windows kann ja nicht erraten, was es sonst machen sollte.
Zitat:

Zitat von zorro08
o Es gibt einen Totalstillstand, wenn zuerst cs.leave ohne vorheriges cs.enter aufgerufen wird

Aha! Sowas macht man aber auch nicht. Du sollst ja die Abschnitte auch so kurz wie möglich halten. Und das sollte man überblicken.


Zitat:

Zitat von zorro08
1. Das Sperren des Hauptthreads klappt mit WaitForSingleObject(Event_0,INFINITE). Und zwar so gut, dass er bei falscher Anwendung niemals wieder ans Laufen kommt.

Sowas schafft man mit allen Syncro-möglichkeiten. Deswegen ist es eben günstiger die Schnittstelle so dünn wie möglich zu machen, und eben am besten gar kein Datenaustausch während der Threadabarbeitung zu machen. Ansonsten möglichst nur eine Syncronisationsart wählen.

Zitat:

Zitat von zorro08
a) Entweder wurde nicht organisiert, dass mindestens ein Event, z.B. von einem Thread, kommt.

Verstehe ich nicht.
Zitat:

Zitat von zorro08
b) Oder mitten in die TimerRoutine platzt dann ein BtnClickEvent, ...

Wie machst du denn das? Wenn beide Ereignisse im selben Thread laufen, dann wird doch der Timer nicht vom Button unterbrochen.

Zitat:

Zitat von zorro08
Die (für mich) richtige Anwendung wäre:
...

Es gibt viele schöne Konzepte. Hier kommt es sehr stark auf den Anwendungsfall an, welches am günstigsten ist.



Zitat:

Zitat von zorro08
o Der Thread führt diese Routine per Synchronize() aus.

In deinem Codebeispiel (vor allem mit mehren Threads) komme ich nicht ganz mit. Aber egal. Aber du mischst hier zwei Syncronisationsmethoden. Achte darauf, dass die eine vollständig abgeschlossen ist, bevor die nächste startet.



Noch kurz was zum Code:
Delphi-Quellcode:
while TRUE do
  begin
    sleep(1);

    if Terminated then
    begin //warum hier ein begin-end-Block?
      exit
    end
Das geht doch insgesamt auch mit (vorrausgesetzt die Reihenfolge mit dem sleep ist unwichtig)
Delphi-Quellcode:
while not terminated do
  begin
    sleep(1);

Dani 10. Mär 2008 14:30

Re: Synchronisieren von hauptthread und Teilthread
 
Vielleicht noch eine Idee. Anstelle eines TTimers könntest du auch den Thread als Timer benutzen, also den Aufruf des Synchronisations-Ereignises von der vergangenen Zeit abhängig machen. (falls das die Sache irgendwie vereinfacht)

zorro08 10. Mär 2008 17:14

Re: Synchronisieren von hauptthread und Teilthread
 
Danke für die bisherigen Hinweise !

Mein Hauptproblem bestand offensichtlich in der Annahme, dass auch im Hauptthread mehrere scheinbar parallelen Prozesse wie

o Timer-Routinen in verschiedenen gleichzeitig sichtbaren Fenstern
o dazwischen auch mal ein BtnClick usw., jeweils mit gewisser Bearbeitungszeit

mit CS.enter/cs.Leave voreinander geschützt werden können.

Das geht - nun ist mir das klar - nicht, weil immer derselbe Thread = Hauptthread sperren soll, dabei aber nur irgendeinen Zähler im CS-Objekt verändert. Alles andere wäre ja auch komplette unsinnig, da sofort ein Deadlock entstehen würde.

Das im Moment noch Rätselhafte für mich ist allerdings, dass durch den Aufruf von cs.enter im Haupthtread bei gleichzeitig laufenden Nebenthreads mit CriticalSections das Programm komplett blockiert wird, also nicht nur die mit cs.enter geschützten Nebenthreads blockieren. Zu einem cs.leave komme ich dann gar nicht mehr.

Noch merkwürdiger: das scheint erst seit kurzem aufzutreten. Ich werde aber nochmals testen, ob das im Zusammenhang mit dem Aufruf von Mainthread-Methoden z.B. einer Form auftritt. Es könnte sein, dass dann gerade die Zeile
Delphi-Quellcode:
Synchronize(form1.DoEtwas)
diesen Effekt bewirkt.


Ich habe mittlerweile den Rat u.a. von Dani beherzigt:

Kritischen Abschnitte des Hauptthreads werden in einen Event gelegt, der vom Nebenthread ausgelöst wurde.

Innerhalb des Events wird dann die Aufgabe ohne Aufruf von KeepWindowsAlive erledigt, so dass parallel dazu weder ein anderer Thread noch der Hauptthread reinspucken kann.

Das scheint zu klappen.

Danke noch mal an alle !


Alle Zeitangaben in WEZ +1. Es ist jetzt 16:10 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz