GUI-MultiThread-Anwendung

Für Fragen von Einsteigern und Programmieranfängern...
Antworten
Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

GUI-MultiThread-Anwendung

Beitrag von photor »

Hallo Forum,

da ich mich als absoluten Anfänger in Bezug auf Multi-Threading betrachte, in dem Forum hier.

Ich habe ein Programm (mit GUI zur Eingabe der Parameter) geschrieben (in Delphi und Lazarus), das ein und dieselbe Rechnung für eine größere Anzahl an Elementen (einige wenige bis zu ca. 120; je nach Problemgröße) durchführen muss. Das funktioniert soweit linear. Aber da bietet sich eine parallele Verarbeitung natürlich an.

Beim Einlesen in das Thema habe ich schnell gelernt, dass GUI und Multi-Thead so seine Fallstricke hat. Also habe ich nach einem kleinen Beispiel gesucht und bin über das hier gestolpert:
https://www.thoughtco.com/synchronizing ... on-1058159
und habe das nachgebaut.

Code: Alles auswählen

unit MT_MainForm;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ComCtrls;

type
    TButton = class(StdCtrls.TButton)
    OwnedThread: TThread;
    ProgressBar: TProgressBar;
  end;

  TMyThread = class(TThread)
  private
    FCounter: Integer;
    FCountTo: Integer;
    FProgressBar: TProgressBar;
    FOwnerButton: TButton;

    procedure DoProgress;
    procedure SetCountTo(const Value: Integer) ;
    procedure SetProgressBar(const Value: TProgressBar) ;
    procedure SetOwnerButton(const Value: TButton) ;

  protected
    procedure Execute; override;

  public
    constructor Create(CreateSuspended: Boolean) ;
    property CountTo: Integer read FCountTo write SetCountTo;
    property ProgressBar: TProgressBar read FProgressBar write SetProgressBar;
    property OwnerButton: TButton read FOwnerButton write SetOwnerButton;
  end;

  { TMainForm }

  TMainForm = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    ProgressBar1: TProgressBar;
    ProgressBar2: TProgressBar;
    ProgressBar3: TProgressBar;
    ProgressBar4: TProgressBar;
    ProgressBar5: TProgressBar;
  private

  public

  end;

var
  MainForm: TMainForm;

implementation

{$R *.lfm}

{ TMyThread }
constructor TMyThread.Create(CreateSuspended: Boolean) ;
begin
  inherited;
  FCounter := 0;
  FCountTo := MAXINT;
end;


procedure TMyThread.DoProgress;
var
  PctDone: Extended;
begin
  PctDone := (FCounter / FCountTo) ;
  FProgressBar.Position := Round(FProgressBar.Step * PctDone) ;
  FOwnerButton.Caption := FormatFloat('0.00 %', PctDone * 100) ;
end;


procedure TMyThread.Execute;
const
  Interval = 1000000;
begin
  FreeOnTerminate := True;
  FProgressBar.Max := FCountTo div Interval;
  FProgressBar.Step := FProgressBar.Max;

  while FCounter < FCountTo do
  begin
    if FCounter mod Interval = 0 then
      Synchronize(DoProgress) ;
      Inc(FCounter) ;
  end;

  FOwnerButton.Caption := 'Start';
  FOwnerButton.OwnedThread := nil;
  FProgressBar.Position := FProgressBar.Max;
end;


procedure TMyThread.SetCountTo(const Value: Integer) ;
begin
  FCountTo := Value;
end;


procedure TMyThread.SetOwnerButton(const Value: TButton) ;
begin
  FOwnerButton := Value;
end;


procedure TMyThread.SetProgressBar(const Value: TProgressBar) ;
begin
  FProgressBar := Value;
end;


procedure TMainForm.Button1Click(Sender: TObject) ;
var
  aButton: TButton;
  aThread: TMyThread;
  aProgressBar: TProgressBar;
begin
  aButton := TButton(Sender) ;
  if not Assigned(aButton.OwnedThread) then
  begin
    aThread := TMyThread.Create(True) ;
    aButton.OwnedThread := aThread;

    aProgressBar :=
      TProgressBar(FindComponent(StringReplace(aButton.Name, 'Button', 'ProgressBar', []))) ;

    aThread.ProgressBar := aProgressBar;
    aThread.OwnerButton := aButton;
    aThread.Resume;
    aButton.Caption := 'Pause';
  end
  else
  begin
    if aButton.OwnedThread.Suspended then
      aButton.OwnedThread.Resume
    else
      aButton.OwnedThread.Suspend;

    aButton.Caption := 'Run';
  end;
end;

end.
Beim Compilieren erhalte ich allerdings folgende Fehlermeldung:

Code: Alles auswählen

Projekt kompilieren, Ziel: MultThread: Exit code 1, Fehler: 1
mt_mainform.pas(12,5) Error: Only classes which are compiled in $M+ mode can be published
mit der ich wenig bis nichts anfangen kann. Es geht um diese Definition:

Code: Alles auswählen

  TButton = class(StdCtrls.TButton)
    OwnedThread: TThread;
    ProgressBar: TProgressBar;
  end;
Was will mir der Autor/Compiler sagen? Ist es ein generelles Problem? Oder Lazarus-spezifisch (der originale Code ist für Delphi)?

Und: gibt es vielleicht einen besseren Einstieg in das Thema Multi-Threading - idealerweise mit (lauffähigem Beispiel-Code)?

Dankbar für jeden Hinweis,
Photor

Benutzeravatar
theo
Beiträge: 10467
Registriert: Mo 11. Sep 2006, 19:01

Re: GUI-MultiThread-Anwendung

Beitrag von theo »

Der Fehler hat mit Multi-threading nichts zu tun.
Er sagt nur, dass die Klasse TThread nicht von TPersistent abgeleitet ist und deshalb nicht "published" sein kann.
Den gleichen Fehler erhältst du so:

Code: Alles auswählen

type
  TTestClass = class
  end;

  TButton = class(StdCtrls.TButton)
    OwnedThread: TTestClass;
    ProgressBar: TStringList;
  end; 
Mach die halt in eine "private" oder "public" Section rein.

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

Re: GUI-MultiThread-Anwendung

Beitrag von Warf »

Mit threading und synchronize solltest du aufpassen, denn was du hier machst:

Code: Alles auswählen

  while FCounter < FCountTo do
  begin
    if FCounter mod Interval = 0 then
      Synchronize(DoProgress) ;
    Inc(FCounter) ;
  end;
Ist im grunde eine regelmäßige zwangsserialisierung. Synchronize führt nämlich nicht nur eine Funktion im mainthread aus, sondern es führt eine Funktion um mainthread aus und wartet bis sie durchgeführt wurde.
Wenn du also mehrere threads hast die gleichzeitig synchronize aufrufen, dann müssen die alle aufeinander warten, dann hast du nicht sehr viel von deiner parallelen ausführung.

Generell finde ich Synchronize an sich keine gute Idee. Es ist damit sehr einfach Thread-safty Probleme zu lösen, aber einfach alles anzuhalten und über den Mainthread zu zwangsserialisieren ist halt schon nicht ganz im Sinne von Multithreading. Also im Grunde erlaubt dir Synchronise sehr einfach schlechten code zu schreiben. Von daher ist mein Vorschlag, am besten vergisst du ganz schnell das Synchronize existiert.

Also nochmal von 0 Anfangen. Bezüglich Threadsynchronisation ist es mit dem GUI eigentlich ganz einfach, das GUI kann nur aus dem Mainthread aktualisiert werden. Also gibt es 2 Optionen: Option 1: Du hast irgendwo den aktuellen status liegen und der Main Thread schaut regelmäßig ob es neue Informationen gibt und wenn ja Updated er das GUI (so genanntes Polling), z.B. über einen Timer:

Code: Alles auswählen

procedure TForm1.Timer1Timer(ASender: TObject);
begin
  ProgressBar1.Position := MyThread.Counter;
end;
Einfach und wenn du eh regelmäßige updates machst auch eine solide lösung. Z.B. für Spiele die eh jeden Frame aktualisieren, die können sich einfach immer die frischesten Werte von nem Thread holen.

Die Zweite option ist das du den Mainthread anhauen kannst, das der doch bitte was machen soll. So ähnlich funktioniert ja auch Synchronize (Warnung: Nach dem lesen dieses Satzes bitte sofort wieder vergessen), aber viel besser dafür ist TThread.Queue, was aber den vorteil hat das der aktuelle Thread nicht stehen bleibt, und du somit keine Zwangsserialisierung hast.

Soweit so einfach, doch ein problem das man mit threading immer wieder hat sind race conditions, 2 threads die den selben speicher gleichzeitig anpacken. Beispiel:

Code: Alles auswählen

type TTest = record
  Val1, Val2: Integer;
end;
...
Data.Val1 := 42;
Data.Val2 := 32;
Hier wird der Data record geschrieben, aber wenn der Main Thread jetzt Data auslesen würde nachdem Val1 geschrieben wurde aber bevor Val2 geschrieben würde, dann wären die Daten inkonsistent, weil Val1 schon den neuen wert hat während Val2 noch den alten wert hat.

Das kann man mit locks lösen, z.B. Critical Sections: https://www.freepascal.org/docs-html/prog/progse45.html

Code: Alles auswählen

EnterCriticalSection(DataCS);
try
  Data.Val1 := 42;
  Data.Val2 := 32;
finally
  LeaveCriticalSection(DataCS);
end;

// Auslesen:
EnterCriticalSection(DataCS);
try
  UpdateGUI(Data);
finally
  LeaveCriticalSection(DataCS);
end;
In der Critical Section kann immer nur ein Thread gleichzeitig sein. Wenn grade ein anderer drin ist muss dein Thread warten. Im gegensatz zu synchronize (Was wie wir ja wissen zum glück nicht existiert), wird hier also nicht immer gewartet, sondern nur wenn auch wirklich eine Kollision stattfindet.

Allerdings sind Locks auch kein Allheilmittel. Es ist nicht so schlimm wie Synchronize (was auch immer das ist), aber hat trozdem einen Einfluss auf die Performance (Locks sind sehr langsam, wenn du in ein Lock läufst kostet dich das mehrere Millisekunden, auch wenn die Gelockte operation nur ein paar nanos braucht), von daher willst du sie falls möglich umgehen. Außerdem erlauben Locks so genannte deadlocks, wenn du 2 locks hast und Thread1 wartet auf Lock2 was von Thread 2 gehalten wird, der auf Lock1 wartet was von Thread 1 gehalten wird. Von daher sollte man immer versuchen Locks nur minimal zu halten (also z.B. den Data record schreiben oder zum lesen eine kopie machen und danach lock verlassen und alle berechnungen für Data außerhalb des Locks machen) und niemals mehrere Locks aufeinmal nehmen.

Dennoch ist das ein ziemliches Problem, von daher sollte man versuchen wenn möglich Locks einfach gänzlich zu vermeiden. Dafür gibt es die interlocked Funktionen der RTL:
InterlockedCompareExchange
InterlockedCompareExchange64
InterlockedCompareExchangePointer
InterlockedDecrement
InterlockedDecrement64
InterlockedExchange
InterlockedExchange64
InterlockedExchangeAdd
InterlockedExchangeAdd64
InterlockedIncrement
InterlockedIncrement64
Welche atomar arbeiten, also mehrere operationen in einem ausführen:

Code: Alles auswählen

program Project1;

{$mode objfpc}{$H+}

uses
  SysUtils,
  Classes;

var
  Counter: Integer;

procedure Increment100000;
var
  i: Integer;
begin
  Sleep(10);
  for i:= 0 to 100000 do
    Inc(Counter);
end;

procedure Increment100000ThreadSafe;
var
  i: Integer;
begin  
  Sleep(10);
  for i:= 0 to 100000 do
    InterlockedIncrement(Counter);
end;

begin
  Counter := 0;
  TThread.CreateAnonymousThread(@Increment100000).Start;
  TThread.CreateAnonymousThread(@Increment100000).Start;
  Sleep(1000); // Warten bis beide threads fertig
  WriteLn(Counter); // 103500 weil nicht thread safe und ihre werte während dem addieren gegenseitig überschreiben
  Counter := 0;
  TThread.CreateAnonymousThread(@Increment100000ThreadSafe).Start;
  TThread.CreateAnonymousThread(@Increment100000ThreadSafe).Start;
  Sleep(1000);
  WriteLn(Counter); // 200002 wie erwartet weil thread safe
  ReadLn;
end. 
Damit ist es möglich thread safe code zu schreiben ohne locks zu verwenden.

Um komplexere daten Threadsafe ohne locks zu übergeben, kannst du einfach eine kopie der daten erzeugen, die dann ganz dem thread gehört. Die LCL bietet dafür Application.QueueAsyncCall, was im grunde das gleiche ist wie TThread.Queue nur das man noch zusätzliche daten angeben kann. Also der thread erstellt eine Kopie der daten, und übergibt die dann an den Mainthread via QueueAsyncCall, und macht dann weiter während der Mainthread sich dann darum kümmern kann das die Daten ins GUI kommen

Code: Alles auswählen

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ComCtrls, StdCtrls;

type
  PUpdateData = ^TUpdateData;
  TUpdateData = record
    Counter: Integer;
  end;

  TMyThread = class(TThread)
  protected
    procedure Execute; override;
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    ProgressBar1: TProgressBar;
    procedure Button1Click(Sender: TObject);
  private
    procedure UpdateProgress(Data: PtrInt);

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TMyThread }

procedure TMyThread.Execute;
var
  Counter: Integer;
  updateData: PUpdateData;
begin
  for Counter:=0 to 100 do
  begin
    // Kopie erstellen
    New(updateData);
    updateData^.Counter := Counter;
    Application.QueueAsyncCall(@Form1.UpdateProgress, PtrInt(updateData));
    Sleep(100);
  end;
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  TMyThread.Create(False);
end;

procedure TForm1.UpdateProgress(Data: PtrInt);
var
  updateData: PUpdateData;
begin
  updateData := PUpdateData(Data);
  ProgressBar1.Position:=updateData^.Counter;
  // Free data nachdem fertig
  Dispose(updateData);
end;

end.
Ohne die LCL kann das gleiche praktisch mit einer globalen variable und InterlockedExchange oder InterlockedCompareExchange gebaut werden aber das wird dann schon wieder und da du ja auch explizit nach GUI anwendungen gesucht hast ist denke ich QueueAsyncCall die beste Option für dich.

Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

Re: GUI-MultiThread-Anwendung

Beitrag von photor »

Hallo theo,
theo hat geschrieben:
Di 10. Mai 2022, 17:20
Der Fehler hat mit Multi-threading nichts zu tun.
Er sagt nur, dass die Klasse TThread nicht von TPersistent abgeleitet ist und deshalb nicht "published" sein kann.
Den gleichen Fehler erhältst du so:

Code: Alles auswählen

type
  TTestClass = class
  end;

  TButton = class(StdCtrls.TButton)
    OwnedThread: TTestClass;
    ProgressBar: TStringList;
  end; 
Mach die halt in eine "private" oder "public" Section rein.
Das werde ich morgen mal ausprobieren und - wenn es geht - versuchen zu verstehen. Danke für den Hinweis.


@warf:
Danke für die ausführliche Antwort. Da muss ich mich aber erstmal rein denken. Kurz nur soviel:
Es gibt die GUI-Anwendung schon und ich will den rechenintensiven Teil parallelisieren. Die Ausführungszeit wird in jedem Fall deutlich verkürzt. Im Prinzip muss in der Ausführungszeit auch niemand das Programm bedienen - kann also eigentlich auch "blockiert" sein (nicht schön, stört aber nicht). Auch, ob ich beim Thread-Wechsel oder sonst ein paar Millisekunden verliere, dürfte nicht so schlimm sein. Der Effekt dadurch, dass ich 3, 4 oder auf den Rechenknechten im Büro auch 20 Threads parallel rechnen lassen kann, wird das wieder aufwiegen.

Aber wie gesagt, ich muss mich durch deine Antwort durcharbeiten; dann kommen bestimmt noch fragen.

PS: warum findet man eigentlich kein einfaches laufendes Einstiegsprogramm zu dem Thema? Das gezeigte ist wirklich das einzige komplette, was ich zum Thema Threads und GUI unter Lazarus/Delphi gefunden habe (ich suche aber weiter).

Ciao,
Photor

Benutzeravatar
theo
Beiträge: 10467
Registriert: Mo 11. Sep 2006, 19:01

Re: GUI-MultiThread-Anwendung

Beitrag von theo »

photor hat geschrieben:
Di 10. Mai 2022, 19:53
PS: warum findet man eigentlich kein einfaches laufendes Einstiegsprogramm zu dem Thema? Das gezeigte ist wirklich das einzige komplette, was ich zum Thema Threads und GUI unter Lazarus/Delphi gefunden habe (ich suche aber weiter).
Man findet doch einiges zu dem Thema in "Werkzeuge" -> "Beispielprojekte".

mt.png
mt.png (53.79 KiB) 2523 mal betrachtet

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

Re: GUI-MultiThread-Anwendung

Beitrag von Warf »

photor hat geschrieben:
Di 10. Mai 2022, 19:53
@warf:
Danke für die ausführliche Antwort. Da muss ich mich aber erstmal rein denken. Kurz nur soviel:
Es gibt die GUI-Anwendung schon und ich will den rechenintensiven Teil parallelisieren. Die Ausführungszeit wird in jedem Fall deutlich verkürzt. Im Prinzip muss in der Ausführungszeit auch niemand das Programm bedienen - kann also eigentlich auch "blockiert" sein (nicht schön, stört aber nicht). Auch, ob ich beim Thread-Wechsel oder sonst ein paar Millisekunden verliere, dürfte nicht so schlimm sein. Der Effekt dadurch, dass ich 3, 4 oder auf den Rechenknechten im Büro auch 20 Threads parallel rechnen lassen kann, wird das wieder aufwiegen.

Aber wie gesagt, ich muss mich durch deine Antwort durcharbeiten; dann kommen bestimmt noch fragen.

PS: warum findet man eigentlich kein einfaches laufendes Einstiegsprogramm zu dem Thema? Das gezeigte ist wirklich das einzige komplette, was ich zum Thema Threads und GUI unter Lazarus/Delphi gefunden habe (ich suche aber weiter).

Ciao,
Photor
Das Problem ist nicht das der Main Thread einfriert (auch wenn das auch doof ist weil Windows dir dann dein Programm abschießen will), sondern das synchronize den Thread der es ausführt einfriert bis die Operation durchgeführt wird. Wenn du also 20 threads hast, die regelmäßig synchronize aufrufen, dann müssen die immer warten bis das synchronize vom anderen thread durchgelaufen ist. Effektiv bedeutet das das jeder thread immer auf die anderen threads warten muss bevor er weiter machen kann, und ab dem punkt hast du halt keine parallelisierung mehr, du hast 20 threads die 90% der Zeit mit warten beschäftigt sind, und damit kann es passieren das das am ende ineffizienter ist als ein single threaded programm was dafür nie wartet.

Ich habe das in meinem Beitrag oben vielleicht ein bisschen als Witz geschrieben mit dem du sollst Synchronize vergessen, aber an sich ist das schon mein ernst gewesen. Synchronize ist in so gut wie allen Fällen die schlechtes mögliche alternative für Thread Synchronisation, und wird nur benutzt weil es einfach ist. Aber einfach Fehler zu machen ist nicht unbedingt was gutes. Synchronize zu benutzen ist eigentlich immer ein Fehler

Wenn du dein GUI nur updaten musst, dann ist Application.QueueAsyncCall (mein letztes beispiel im vorrigen post) eigentlich genau das was du haben willst. Es garantiert Datenkonsistenz da eine neue Kopie der Daten nur für den Mainthread erstellt wird und von daher der Arbeitsthread nicht mehr dran rumpfuscht während der MainThread damit arbeitet

Beispiel:

Code: Alles auswählen

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ComCtrls, StdCtrls;

type

  { TTestThread }

  TSyncThread = class(TThread)
  private
    FProgressBar: TProgressBar;
    FCount: Integer;
    procedure Update;
  protected
    procedure Execute; override;
  public
    constructor Create(ProgBar: TProgressBar);
  end;

  { TQueueThread }

  TQueueThread = class(TThread)
  private
    FProgressBar: TProgressBar;
    procedure Update(Data: IntPtr);
  protected
    procedure Execute; override;
  public
    constructor Create(ProgBar: TProgressBar);
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    ProgressBar1: TProgressBar;
    ProgressBar2: TProgressBar;
    ProgressBar3: TProgressBar;
    ProgressBar4: TProgressBar;
    ProgressBar5: TProgressBar;
    ProgressBar6: TProgressBar;
    ProgressBar7: TProgressBar;
    ProgressBar8: TProgressBar;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    FStartTime, FEndTime: QWord;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TQueueThread }

procedure TQueueThread.Update(Data: IntPtr);
begin
  FProgressBar.Position := PInteger(Data)^;
  Dispose(PInteger(Data));
end;

procedure TQueueThread.Execute;
var
  PCount: PInteger;
  i: Integer;
begin
  Sleep(100);
  for i:=0 to 100000 do
  begin
    if i mod 20 = 0 then
    begin
      new(PCount);
      PCount^ := i;
      Application.QueueAsyncCall(@Update, IntPtr(PCount));
    end;
  end;
  Form1.FEndTime := GetTickCount64;
end;

constructor TQueueThread.Create(ProgBar: TProgressBar);
begin
  FProgressBar := ProgBar;
  FreeOnTerminate:=True;
  inherited Create(False);
end;

{ TTestThread }

procedure TSyncThread.Update;
begin
  FProgressBar.Position:=FCount;
end;

procedure TSyncThread.Execute;
var
  i: Integer;
begin
  Sleep(100);
  for i:=0 to 100000 do
  begin
    if FCount mod 20 = 0 then
      Synchronize(@Update);
    Inc(FCount);
  end;
  Form1.FEndTime := GetTickCount64;
end;

constructor TSyncThread.Create(ProgBar: TProgressBar);
begin
  FProgressBar := ProgBar;
  FCount:=0;
  FreeOnTerminate:=True;
  inherited Create(False);
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  Form1.FStartTime := GetTickCount64;
  TSyncThread.Create(ProgressBar1);
  TSyncThread.Create(ProgressBar2);
  TSyncThread.Create(ProgressBar3);
  TSyncThread.Create(ProgressBar4);
  TSyncThread.Create(ProgressBar5);
  TSyncThread.Create(ProgressBar6);
  TSyncThread.Create(ProgressBar7);
  TSyncThread.Create(ProgressBar8);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Form1.FStartTime := GetTickCount64;
  TQueueThread.Create(ProgressBar1);
  TQueueThread.Create(ProgressBar2);
  TQueueThread.Create(ProgressBar3);
  TQueueThread.Create(ProgressBar4);
  TQueueThread.Create(ProgressBar5);
  TQueueThread.Create(ProgressBar6);
  TQueueThread.Create(ProgressBar7);
  TQueueThread.Create(ProgressBar8);
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  ShowMessage((FEndTime-FStartTime).ToString);
end;

end.
8 Threads die jeweils eine progressbar updaten, entweder über QueueAsyncCall oder über synchronize. QueueAsyncCall benötigt auf meinem rechner c.a. 400ms um ausgeführt zu werden während synchronize c.a. 800ms braucht.
Das problem bei locking, wie hier durch synchronize ist tatsächlich das es mit mehr threads schlechter skaliert. Bei nur 4 threads mit 4 progressbars braucht die Synchronize variante 400ms und die QueueAsyncCall 300ms, also die Verdopplung des Parallelisierungsgrades hat auch den Zeitaufwand verdoppelt, es ist damit also tatsächlich nicht besser als 2 mal 4 threads laufen zu lassen bei Synchronize, während bei QueueAsyncCall nur 30% schlechter ist
Das bedeutet je mehr threads du machst, desto geringer ist der parallelisierungsgrad bei Synchronize, bis zu dem punkt wo es schlechter ist als weniger threads zu haben während er bei QueueAsyncCall deutlich besser skaliert mit der anzahl an threads.

Wie gesagt, es gibt eigentlich keinen Grund Synchronize auch nur anzuschauen, in jeder Situation gibt es eine Lösung die massiv viel besser ist, auch wenn sie ein bisschen mehr arbeit benötigt

Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

Re: GUI-MultiThread-Anwendung

Beitrag von photor »

theo hat geschrieben:
Di 10. Mai 2022, 20:20
Man findet doch einiges zu dem Thema in "Werkzeuge" -> "Beispielprojekte".
OK. Überzeugt! Du hast Recht. Da hab ich mich tatsächlich noch nie hin verirrt.

Ich wühle mich mal rein.

Ciao,
Photor

Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

Re: GUI-MultiThread-Anwendung

Beitrag von photor »

Warf hat geschrieben:
Mi 11. Mai 2022, 09:57
[... viel weg gelassen ...]
Beispiel:

Code: Alles auswählen

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ComCtrls, StdCtrls;

type

  { TTestThread }

  TSyncThread = class(TThread)
  private
    FProgressBar: TProgressBar;
    FCount: Integer;
    procedure Update;
  protected
    procedure Execute; override;
  public
    constructor Create(ProgBar: TProgressBar);
  end;

  { TQueueThread }

  TQueueThread = class(TThread)
  private
    FProgressBar: TProgressBar;
    procedure Update(Data: IntPtr);
  protected
    procedure Execute; override;
  public
    constructor Create(ProgBar: TProgressBar);
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    ProgressBar1: TProgressBar;
    ProgressBar2: TProgressBar;
    ProgressBar3: TProgressBar;
    ProgressBar4: TProgressBar;
    ProgressBar5: TProgressBar;
    ProgressBar6: TProgressBar;
    ProgressBar7: TProgressBar;
    ProgressBar8: TProgressBar;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    FStartTime, FEndTime: QWord;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TQueueThread }

procedure TQueueThread.Update(Data: IntPtr);
begin
  FProgressBar.Position := PInteger(Data)^;
  Dispose(PInteger(Data));
end;

procedure TQueueThread.Execute;
var
  PCount: PInteger;
  i: Integer;
begin
  Sleep(100);
  for i:=0 to 100000 do
  begin
    if i mod 20 = 0 then
    begin
      new(PCount);
      PCount^ := i;
      Application.QueueAsyncCall(@Update, IntPtr(PCount));
    end;
  end;
  Form1.FEndTime := GetTickCount64;
end;

constructor TQueueThread.Create(ProgBar: TProgressBar);
begin
  FProgressBar := ProgBar;
  FreeOnTerminate:=True;
  inherited Create(False);
end;

{ TTestThread }

procedure TSyncThread.Update;
begin
  FProgressBar.Position:=FCount;
end;

procedure TSyncThread.Execute;
var
  i: Integer;
begin
  Sleep(100);
  for i:=0 to 100000 do
  begin
    if FCount mod 20 = 0 then
      Synchronize(@Update);
    Inc(FCount);
  end;
  Form1.FEndTime := GetTickCount64;
end;

constructor TSyncThread.Create(ProgBar: TProgressBar);
begin
  FProgressBar := ProgBar;
  FCount:=0;
  FreeOnTerminate:=True;
  inherited Create(False);
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  Form1.FStartTime := GetTickCount64;
  TSyncThread.Create(ProgressBar1);
  TSyncThread.Create(ProgressBar2);
  TSyncThread.Create(ProgressBar3);
  TSyncThread.Create(ProgressBar4);
  TSyncThread.Create(ProgressBar5);
  TSyncThread.Create(ProgressBar6);
  TSyncThread.Create(ProgressBar7);
  TSyncThread.Create(ProgressBar8);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Form1.FStartTime := GetTickCount64;
  TQueueThread.Create(ProgressBar1);
  TQueueThread.Create(ProgressBar2);
  TQueueThread.Create(ProgressBar3);
  TQueueThread.Create(ProgressBar4);
  TQueueThread.Create(ProgressBar5);
  TQueueThread.Create(ProgressBar6);
  TQueueThread.Create(ProgressBar7);
  TQueueThread.Create(ProgressBar8);
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  ShowMessage((FEndTime-FStartTime).ToString);
end;

end.
[...]
Hallo Warf,

gerade mal platt abgeschrieben und ausgeführt. Die Zahlen sehen hier (ThinkPad T440s, 4 Kerne) ähnlich aus:
  • Button1: 263 (ms denke ich)
  • Button2: 133
Es ist ein erstes Erfolgserlebnis, ja. Aber ich muss jetzt mal genau durchsteigen und sehen, wie ich das auf mein "Problem" anwende. Das gilt auch für theos Hinweis (Vorschlag: ein Hinweis auf den Wiki-Seiten auf die entsprechenden Beispiele)

Danke,
Photor

Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

Re: GUI-MultiThread-Anwendung

Beitrag von photor »

Hallo Forum,

ich habe in der Zwischenzeit ein bisschen - ausgehend von einem der mitgelieferten Beispiele - rum probiert, bin aber jetzt an einem Punkt angekommen, wo ich nochmal Input brauche.

Es handelt sich um ein GUi-Programm, dass als Workload Wurzeln im Intervallschachtelungsverfahren zieht (ich musste aber trotzdem noch ein Delay einbauen müssen). Insgesammt sollen 25 Wurzeln (von 1 bis 25) gezogen werden; das soll parallel geschehen, aber nur mit mit 3 Threads parallel (weil mein Lappy nur 4 Kerne hat; später soll ein anderer Workload mit ca. 90 aufwändigeren Berechnungen auf bis zu ca. 20 Kernen parallel laufen).

Das Starten der Threads scheint zu klappen, genau so wie das Aufteilen in 3-er Päckchen (cooler wäre natürlich, wenn immer ein neuer Thread gestartet wird, wenn ein anderer fertig wurde - das aber später). Mir geht es gerade darum, wie bekomme ich das Programm dazu, zeitnah mit dem GUI zu interagieren? Ich kann zwar Text in dem Log-Bereich (eine ListBox) ausgeben; die Ausgabe kommt aber erst zum Ende aller Threads. Genauso wäre eine ProgressBar schön (dazu gibt es noch keinen Code) oder wenn bereits berechneten Ergebnisse im Ergebnis-StringGrid ausgegeben werden könnte, sobald berechnet.

Den aktuellen Stand habe ich zusammen gezipt und angehängt (ich hoffe, ich habe alle wichtigen Dateien erwischt).

Ciao,
Photor


PS: Lazarus 2.2.0, FPC: 3.2.2 auf ArchLinux
Dateianhänge
MultiThread.zip
(58.24 KiB) 59-mal heruntergeladen

Benutzeravatar
kupferstecher
Beiträge: 418
Registriert: Do 17. Nov 2016, 11:52

Re: GUI-MultiThread-Anwendung

Beitrag von kupferstecher »

photor hat geschrieben:
Mi 25. Mai 2022, 20:34
Mir geht es gerade darum, wie bekomme ich das Programm dazu, zeitnah mit dem GUI zu interagieren? Ich kann zwar Text in dem Log-Bereich (eine ListBox) ausgeben; die Ausgabe kommt aber erst zum Ende aller Threads.
Du blockierst den Main-Thread ja aktiv:

Code: Alles auswählen

    // wait until threads are finished
    repeat
      [...]
    until AllFinished;
In der Zeit kann auf der GUI nichts passieren. Bei Zugriffen auf die Listbox werden die Daten dort abgelegt und über die Messagequeue ein Neuzeichnen angefordert. (Im Control geschieht das vermutlich über ein Invalidate;) Die Messagequeue wird aber erst abgearbeitet, wenn der Mainthread nicht mehr blockiert ist. Das ist absichtlich so implementiert, damit kann man viele einzelne kleine Änderungen an einem Control vornehmen und es muss anschließend nur einmal neu gezeichnet werden. Auch vermeidet das ein Flackern. Bspw. wenn man einen Text ändert und die Höhe und Breite danach anpasst, ist durch diesen Mechanismus gar kein Zwischenschritt sichtbar. Übrigens scheint mir dein Code bzgl. der Listbox nicht Thread-Save zu sein. Auf Controls darf nur aus dem Main-Thread raus zugegriffen werden.

Eine Möglichkeit um das GUI nicht zu blockieren (aber eher ein Hack) wäre Application.ProcessMessages in die Warte-Schleife einzubauen. Darin wird dann die Message-Queue abgearbeitet. Besser ist m.E. aber an der Stelle die Procedur zu verlassen. Der Code zum Aufräumen muss dann in eine eigene Procedur gepackt werden, die beim Beenden der Threads aufgerufen wird. Da ja vorher nicht klar ist, welcher Thread zuletzt terminieren wird, muss man das beim jeweiligen Beenden mitverfolgen, z.B. über einen Zähler oder eine Liste.

In deiner jetzigen Version lastet die Schleife den Kern übrigens voll aus, ein sleep(0) würde helfen, es wird dort ja sowieso nicht gerechnet. Und dann kannst du auch alle 4 Kerne für Rechenthreads "verplanen", der Main-Thread verursacht, wenn er normal läuft, ja nur eine minimale CPU-Belastung.

Warum es kaum Komplettbeispiele gibt? Ich denke die einzelnen Konzepte sind nicht so schwierig, aber man benötigt für ein sinnvolles Multithreading-Programm mehrere der Konzepte und dann wird es schnell unübersichtlich.

Übrigens fehlt die .lpr in deinem ZIP.

Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

Re: GUI-MultiThread-Anwendung

Beitrag von photor »

kupferstecher hat geschrieben:
Do 26. Mai 2022, 10:08
photor hat geschrieben:
Mi 25. Mai 2022, 20:34
Mir geht es gerade darum, wie bekomme ich das Programm dazu, zeitnah mit dem GUI zu interagieren? Ich kann zwar Text in dem Log-Bereich (eine ListBox) ausgeben; die Ausgabe kommt aber erst zum Ende aller Threads.
Du blockierst den Main-Thread ja aktiv:

Code: Alles auswählen

    // wait until threads are finished
    repeat
      [...]
    until AllFinished;
In der Zeit kann auf der GUI nichts passieren. Bei Zugriffen auf die Listbox werden die Daten dort abgelegt und über die Messagequeue ein Neuzeichnen angefordert. (Im Control geschieht das vermutlich über ein Invalidate;) Die Messagequeue wird aber erst abgearbeitet, wenn der Mainthread nicht mehr blockiert ist. Das ist absichtlich so implementiert, damit kann man viele einzelne kleine Änderungen an einem Control vornehmen und es muss anschließend nur einmal neu gezeichnet werden. Auch vermeidet das ein Flackern. Bspw. wenn man einen Text ändert und die Höhe und Breite danach anpasst, ist durch diesen Mechanismus gar kein Zwischenschritt sichtbar. Übrigens scheint mir dein Code bzgl. der Listbox nicht Thread-Save zu sein. Auf Controls darf nur aus dem Main-Thread raus zugegriffen werden.
Wenn ich das richtig verstehe, dann muss ich in der oben genannten Schleife eine Funktion aufrufen, die explizit die GUI-Operationen ausführt, die ich haben will (Log updaten, Ergebnisse in StringGrid eintragen, ProgressBar anpassen).
kupferstecher hat geschrieben:
Do 26. Mai 2022, 10:08
Eine Möglichkeit um das GUI nicht zu blockieren (aber eher ein Hack) wäre Application.ProcessMessages in die Warte-Schleife einzubauen. Darin wird dann die Message-Queue abgearbeitet. Besser ist m.E. aber an der Stelle die Procedur zu verlassen. Der Code zum Aufräumen muss dann in eine eigene Procedur gepackt werden, die beim Beenden der Threads aufgerufen wird. Da ja vorher nicht klar ist, welcher Thread zuletzt terminieren wird, muss man das beim jeweiligen Beenden mitverfolgen, z.B. über einen Zähler oder eine Liste.
"beim Beenden der Threadsaufgerufen wird" bedeutet, dass ich eine Procedure UpdateGUI(parameter) schreibe und die am Ende der des MyThread.Execute aufrufe?
kupferstecher hat geschrieben:
Do 26. Mai 2022, 10:08
In deiner jetzigen Version lastet die Schleife den Kern übrigens voll aus, ein sleep(0) würde helfen, es wird dort ja sowieso nicht gerechnet. Und dann kannst du auch alle 4 Kerne für Rechenthreads "verplanen", der Main-Thread verursacht, wenn er normal läuft, ja nur eine minimale CPU-Belastung.
Zum Verständnis: MainThread meint meinen normalen Programmablauf? Einen expliziten MainThread habe ich ja nicht definiert.
kupferstecher hat geschrieben:
Do 26. Mai 2022, 10:08
Übrigens fehlt die .lpr in deinem ZIP.
Ups. Sorry. Hier noch mal mit .lpr.

Bis hierher schon mal Danke. Das waren ein paar neue Ansätze für mich zum Probieren.

Ciao,
Photor
Dateianhänge
MultiThread.zip
(58.65 KiB) 59-mal heruntergeladen

Benutzeravatar
kupferstecher
Beiträge: 418
Registriert: Do 17. Nov 2016, 11:52

Re: GUI-MultiThread-Anwendung

Beitrag von kupferstecher »

photor hat geschrieben:
Do 26. Mai 2022, 10:39
Wenn ich das richtig verstehe, dann muss ich in der oben genannten Schleife eine Funktion aufrufen, die explizit die GUI-Operationen ausführt, die ich haben will (Log updaten, Ergebnisse in StringGrid eintragen, ProgressBar anpassen).
Ich weiß jetzt nicht, ob ich dich richtig verstehe:) Es reicht ein Application.ProcessMessages;
dann werden die Controls aktualisiert.
Also

Code: Alles auswählen

  // wait until threads are finished
  repeat
    [...]
    Application.ProcessMessages;
    sleep(10);
  until AllFinished;
Aber aufpassen, die Buttons können damit auch wieder geklickt werden, die Berechnungen können also doppelt gestartet werden, was u.U. zu Problemen führt.
kupferstecher hat geschrieben:
Do 26. Mai 2022, 10:08
Besser ist m.E. aber an der Stelle die Procedur zu verlassen. Der Code zum Aufräumen muss dann in eine eigene Procedur gepackt werden, die beim Beenden der Threads aufgerufen wird. Da ja vorher nicht klar ist, welcher Thread zuletzt terminieren wird, muss man das beim jeweiligen Beenden mitverfolgen, z.B. über einen Zähler oder eine Liste.
"beim Beenden der Threadsaufgerufen wird" bedeutet, dass ich eine Procedure UpdateGUI(parameter) schreibe und die am Ende der des MyThread.Execute aufrufe?
Mit "am Ende des MyThread.Execute" wieder aufpassen! Alles in Execute findet im Thread-Kontext statt. Besser die Methode TThread.OnTerminate verwenden, diese wird im GUI-Thread (= Main-Thread) ausgeführt und ist damit sicher.
https://www.freepascal.org/docs-html/cu ... inate.html
Zum Verständnis: MainThread meint meinen normalen Programmablauf? Einen expliziten MainThread habe ich ja nicht definiert.
Genau, das Programm wird in einem Thread gestartet und den bezeichnet man als Main-Thread. Nur der ist für die GUI verantwortlich.

Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

Re: GUI-MultiThread-Anwendung

Beitrag von photor »

Moin,

Danke für deine Geduld und deine Erklärungen; mir ist einiges klarer jetzt und ich werde es ausprobieren. Dieses ganze Multi-Threading ist absolut neu für mich (bisher immer alles schön linear!).

Ciao,
Photor

Benutzeravatar
photor
Beiträge: 443
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.6 FPC 3.2.2 (Gtk2)
CPU-Target: 64Bit

Re: GUI-MultiThread-Anwendung

Beitrag von photor »

Hallo Forum,

sorry, dass ich diesen alten Thread nochmal vorhole, aber es erspart mir, alles noch mal neu zu erklären.

Stand ist folgender:
  1. Ich konnte das das angesprochene Programm mittlerweile mittels TThread-Class quasi von Hand parallelisieren. Der Erfolg: vom ca. 50 Sek. für mein Test-Problem bei linearer Rechnung auf ca. 20 Sek bei 4 parallelen Threads; nicht sooo doll. Aber das Prinzip funktioniert und während der Rechnung wird sogar ein ProgressBar aktualisiert.

    Dabei werden aus den 100 Berechnungstasks immer bis zu 4 (Anzahl Cores of dem Lappy) bearbeitet, dann das Ergebnis und ProgessBar ge-update-t und dann die nächste Gruppe.
  2. Zusätzlich bin ich über MTProc aus dem Package MultiThreadProcsLaz gestolpert. Das bringt für ein einfaches Test-Problem (mittels ProcThreadPool.DoParallel) einen weiteren Faktor von ca. 2 in der Geschwindigkeit.

    Nachteil ist aber:
    1. Die Tasks werden in einer nicht festgelegten Reihenfolge bearbeitet (nicht schlimm, aber man muss beim Einsammeln der Ergebnisse aufpassen)
    2. Das Programm bzw. die GUI ist blockiert, bis alle (hier 100) Tasks durch sind; ein ProgressBar macht keinen Sinn und die Ergebnisse liegen auch erst am Ende vor. Um das künstlich zu forcieren, könnte ich natürlich wieder kleiner Untermengen rechnen (z.B. 10 x 10) und dann die Zwischenarbeiten tun. Das ist aber irgendwie auch Holzhammer und unelegant.
    Ich habe schon ein bisschen im Netz und sogar in den Sourcen geschaut finde aber keinen Hinweis darauf, ob man einen "Zwischenstand" (= bereits bearbeitete Threads) abfragen kann. Es gibt ja einige Parameter, mit denen sich das verhalten beeinflussen kann (z.B. MaxThreadCount).
Vielleicht kennt sich ja hier jemand besser mit MTProc aus und hat einen Tipp wie man dessen Verhalten modifizieren kann. Dankbar für jeden Hinweis.

Ciao,
Photor

Antworten