GUI-MultiThread-Anwendung

Für Fragen von Einsteigern und Programmieranfängern...
Antworten
Benutzeravatar
photor
Beiträge: 287
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.0 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: 8987
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: 1646
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: 287
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.0 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: 8987
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) 333 mal betrachtet

Warf
Beiträge: 1646
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: 287
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.0 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: 287
Registriert: Mo 24. Jan 2011, 21:38
OS, Lazarus, FPC: Arch Linux: L 2.2.0 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

Antworten