Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   GUI-Design mit VCL / FireMonkey / Common Controls (https://www.delphipraxis.net/18-gui-design-mit-vcl-firemonkey-common-controls/)
-   -   Delphi [Threads] Textureloader - Einige Fragen (https://www.delphipraxis.net/124538-%5Bthreads%5D-textureloader-einige-fragen.html)

Zwoetzen 20. Nov 2008 16:48


[Threads] Textureloader - Einige Fragen
 
Hi alle zusammen,

ich bin gerae dabei, eine OpenGL-Anwendung zu schreiben. In diesem Programm müssen je nach aktueller Situation Texturen nachgeladen werden (eben die, die als nächstes gebraucht werden). Das ganze möchte ich (bzw. habe ich schon) in einen Thread auslagern, um das Programm selbst nicht unnötig zu stören.

Unten habt ihr den entsprechenden Quellcode von meiner TTextureLoader-Klasse.

Meine Fragen diesbezüglich sind nun folgende:
  • Ich habe gehört, dass man für gemeinsam genutzten Speicher CriticalSections einsetzen muss. Ich denke, dass dies bei meiner FQueue der Fall ist. Doch wie genau muss das hier aussehen? Einfach um die ganzen FQueue-Methoden-Afurufen dieses Enter/Leave-Dingens drumsetzen?
  • Um nicht immer einen neuen Thread anlegen zu müssen, versuche ich, den einen, der bei Programmstart angelegt wird, am Leben zu behalten. Ist die Vorgehensweise, den Thread bei "arbeitslosigkeit" schlafen zu legen und beim Hinzufügen eines neuen Jobs wieder aufzuwekcen elegant oder sollte man sowas vermeiden? ^^ (Will keinen Müll schreiben, deshalb diese Frage ;))
  • Zum Verständnis: Was genau macht eigentlich Synchronize und wann muss ich es verwenden? Ich habe gelesen, dass das irgendwas mit dem Context des Threads zu tun hat, weswegen das GenTexture() auch mit Synchronize aufgerufen werden muss. Aber inwiefern muss ich andere Methoden ebenfalls damit aufrufen (wie zB meine FinishProc der Jobs, SyncSucceeded und SyncFailed)?
  • Wie ist das, wenn ich den Thread am Ende wieder freigeben will: Mein logischer Verstand sagt mir, dass ein Terminate() allein nicht reichen kann, da der Thread ja eventuell noch schläft. Muss ich also vorher ein Resume() aufrufen, und danach ein Terminate(), oder andersrum? (Bei ersterem könnte es ja passieren, dass der Thread sofort wieder wegnickt, weil die FQueue ja noch leer ist. Bei zweiterem bin ich aber nicht sicher, ob das richtig ist... :gruebel: )


Würde mich freuen, wenn ihr ein paar erklärende Worte niederschreiben könntet, damit ich zum einen keine Fehler einbaue, und zum anderen den Sachverhalt Threads wieder ein wenig mehr verstehe ;)

MfG Zwoetzen
____________________________________

Delphi-Quellcode:
type
  TFinishProc = procedure(Succeeded: Boolean) of Object;

  TJob = class
    Path:    String;
    Texture: TglBitmap2D;
    OnFinish: TFinishProc;
  end;

  TTextureLoader = class(TThread)
    private
      FQueue: TObjectQueue; // Speichert die aktuellen Aufträge
      FCurJob: TJob;        // Aktueller Job (für die Synchronize-Prozeduren)

      procedure SyncGenTex;
      procedure SyncFailed;
      procedure SyncSucceeded;
    public
      constructor Create;
      destructor Destroy; override;

      procedure AddTexture(Path: String; Texture: TglBitmap2D;
                           OnFinish: TFinishProc = nil);
      procedure Execute; override;
  end;

[...]
 
procedure TTextureLoader.AddTexture(Path: string; Texture: TglBitmap2D;
                                    OnFinish: TGFinishProc = nil);
// Das Texture-Objekt wird immer schon vor Aufruf dieser Funktion angelegt.
// OnFinish bietet die Möglichkeit, "Bescheid" zu geben, wenn die Textur fertig ist oder nicht geladen werden kann
var
  Job: TJob;
begin
  if FileExists(Path) and Assigned(Texture) then begin
    Job := TJob.Create;
    Job.Path := Path;
    Job.Texture := Texture;
    Job.OnFinish := OnFinish;

    // Brauch ich für diese Anweisung jetzt eine CriticalSection? (Ich denk mal schon)
    FQueue.Push(Job);

    // Falls Thread angehalten wurde, wieder aufnehmen
    if Suspended then
      Resume;
  end;
end;

procedure TTextureLoader.Execute;
begin
  while not Terminated do begin

    // Schlafen legen, wenn keine Jobs mehr vorhanden sind, um den Thread zu erhalten
    if (FQueue.Count = 0) then
      Suspend
    else begin
      // Hier auch eine CriticalSection?
      FCurJob := MTGJob(fQueue.Pop);

      // Eventuell noch ein try..finally rumbasteln, um FCurJob sicher freizugeben und einen Fehler in SyncFailed abzufangen
      try
        FCurJob.Texture.LoadFromFile(FCurJob.Path);
        Synchronize(SyncGenTex);

        Synchronize(SyncSucceeded);
        FCurJob.Free;
      except
        on E: Exception do
          Synchronize(SyncFailed);
      end; // try..except
    end; // if..then..else

  end; // while
end;

procedure TTextureLoader.SyncGenTex;
begin
  FCurJob.Texture.GenTexture;
end;

// Inwiefern sind die folgenden zwei Synchronize-Prozeduren wirklich erforderlich?
procedure TTextureLoader.SyncFailed;
begin
  if Assigned(FCurJob.OnFinish) then
    FCurJob.OnFinish(False);
end;

procedure TTextureLoader.SyncSucceeded;
begin
  if Assigned(FCurJob.OnFinish) then
    FCurJob.OnFinish(True);
end;

sirius 20. Nov 2008 17:06

Re: [Threads] Textureloader - Einige Fragen
 
  1. Ja, du musst bei der Queue Critical Sections verwenden (ich vermute, du gibst dem Objekt im Maintthread neue Aufgaben). So, wie ich das sehe, kannst du anstatt der Queue auch eine TThreadList nehmen. Ansonsten musst du die Queue in eine Klasse mit einer Critical Section kapseln. Da gibt es auch schon vorbereitete Klassen in der SyncObj-Unit.
  2. Wenn du nur einen Job zu gleichen Zeit hast, finde ich diese Vorgehensweise i.O. Du solltest hier nur nicht mit suspend und resume arbeiten, sondern den Thread mittels Events warten lassen (waitforsingleEvent). Dieses Event könntest du in deine Warteschlange mit rein implementieren. Dadurch kannst du es automatisch feuern, sobald estwas der Warteschlange hinzugefügt wird.
  3. Du hast Synchronize richtig verwendet. Achte darauf, dass synchronize nur funktioniert, wenn du es in einer EXE hast (nicht in eine DLL legen) Synchronize hält deinen Thread an und sagt dem Application-Objekt, des es demnächst mal die übergebene Methode starten soll. Und wenn das Application-Objekt damit fertig ist, kann dein Thread weitermachen. Dadurch ist eben sichergestellt, dass dein Thread nicht gleichzeitig mit dem Mainthread arbeitet. Der Mainthread kann so gefahrlos auf den Speicher des Threads zugreifen.
  4. Ja. Wenn du Punkt zwei umsetzt kannst du ja auch das Event feuern. Oder ein zweites Anlegen.

Zwoetzen 20. Nov 2008 18:22

Re: [Threads] Textureloader - Einige Fragen
 
Danke für die Antwort, Sirius :)

Punkt 3 ist gut erklärt, jetzt habe ich eine bessere Vorstellung, was im Hintergrund beim Aufruf von Synchronize passiert.

Zu den drei anderen Punkten: Ich habe jetzt mal eine TThreadQueue (eigentlich eher eine TThreadObjectQueue, is aber zu lang xD) erstellt, die die CriticalSections enthält, nur bin ich nicht sicher, ob ich das so richtig verstanden und umgesetzt habe. (Hab versucht, mich an die TThreadList zu halten) Vor allem beim Destroy bin ich mir unsicher: Wer wäre jetzt für das Freigeben der noch vorhandenen Jobs verantwortlich? Die ThreadQueue selbst, weil sie diese ja "in Bearbeitung" hat, oder der TextureLoader, der die Jobs angelegt hat? :gruebel: (Hatte mal was gehört von dort Freigeben, wo sie angelegt wurden, demnach müsste sich der TextureLoader drum kümmern.)

Delphi-Quellcode:
type
  TThreadQueue = class(TObjectQueue)
    private
      FLock: TRTLCriticalSection;
    public
      constructor Create;
      destructor Destroy; override;

      function Push(AObject: TObject): TObject;
      function Pop: TObject;
      function Peek: TObject;
  end;

[...]

constructor TThreadQueue.Create;
begin
  inherited Create;
  InitializeCriticalSection(FLock)
end;

destructor TThreadQueue.Destroy;
begin
  EnterCriticalSection(FLock);
  try
    // Entweder die ThreadQueue kümmert sich ums freigeben, oder der TextureLoader muss es...
    while (List.Count <> 0) do
      (inherited Pop).Free;

    inherited Destroy;
  finally
    LeaveCriticalSection(FLock);
    DeleteCriticalSection(FLock);
  end;
end;

function TThreadQueue.Push(AObject: TObject): TObject;
begin
  EnterCriticalSection(FLock);
  try
    Result := inherited Push(AObject);
  finally
    LeaveCriticalSection(FLock);
  end;
end;

function TThreadQueue.Pop: TObject;
begin
  EnterCriticalSection(FLock);
  try
    Result := inherited Pop;
  finally
    LeaveCriticalSection(Flock);
  end;
end;

function TThreadQueue.Peek: TObject;
begin
  EnterCriticalSection(FLock);
  try
    Result := inherited Peek;
  finally
    LeaveCriticalSection(FLock);
  end;
end;
Des weiteren verstehe ich den Punkt mit den Events nicht wirklich. Habe noch nie mit Events gearbeitet, und weiß somit nicht, wie ich das jetzt machen muss. Könntest du (oder jemand anderes ;)) einen Beispielcode geben? (Oder direkt in meine ThreadQueue einbauen, wäre noch besser :mrgreen: )
Nur soviel habe ich herausgefunden: WaitForSingleEvent gibts nicht, sollte wohl WaitForSingleObject heißen :wink:

sirius 20. Nov 2008 22:26

Re: [Threads] Textureloader - Einige Fragen
 
Ahja, WaitforSingleObject. Man kann ja auch auf Threads, Processe, Semaphore ... warten.

Ich würde die ThreadObjectQueue im Textureloader erstellen und zerstören. Ich bastel mal alles zusammen und baue ein Event ein. Keine Garantie, dass es direkt so funktioniert
Delphi-Quellcode:
type
  TThreadQueue = class(TObjectQueue)
    private
      FLock: TRTLCriticalSection; //Es gibt auch die Klasse TCriticalSection, aber es besteht kein Unterschied zur direkten Verwendung der WinAPI-Funktionen
      FEvent: TEvent; //Wie bei der Critical Section, kannst du hier auch direkt die WinAPI nutzen; in der Klasse TEvent sind ein paar Vereinfachungen
    public
      constructor Create;
      destructor Destroy; override;

      function Push(AObject: TObject): TObject;
      function Pop: TObject;
      function Peek: TObject;

      property Event:TEvent read FEvent;
  end;

[...]

constructor TThreadQueue.Create;
begin
  inherited Create;
  InitializeCriticalSection(FLock)
  FEvent:=TEvent.create; //evtl. initialisieren?
end;

destructor TThreadQueue.Destroy;
begin
  EnterCriticalSection(FLock);
  try
    while (List.Count <> 0) do
      (inherited Pop).Free;
    FEvent.free;
    inherited Destroy;
  finally
    LeaveCriticalSection(FLock);
    DeleteCriticalSection(FLock);
  end;
end;

function TThreadQueue.Push(AObject: TObject): TObject;
begin
  //neues Objekt in der Klasse, also Event feuern
  EnterCriticalSection(FLock);
  try
    Result := inherited Push(AObject);
    FEvent.SetEvent;
  finally
    LeaveCriticalSection(FLock);
  end;
end;

function TThreadQueue.Pop: TObject;
begin
  EnterCriticalSection(FLock);
  try
    Result := inherited Pop;
  finally
    LeaveCriticalSection(Flock);
  end;
end;

function TThreadQueue.Peek: TObject;
//Was jetzt peek macht, weis ich nicht, deswegen ändere ich hier nix
begin
  EnterCriticalSection(FLock);
  try
    Result := inherited Peek;
  finally
    LeaveCriticalSection(FLock);
  end;
end;
Delphi-Quellcode:
 TTextureLoader = class(TThread)
    private
      FQueue: TThreadQueue
      FCurJob: TJob;        // Aktueller Job (für die Synchronize-Prozeduren)

      procedure SyncGenTex;
      procedure SyncFailed;
      procedure SyncSucceeded;
    public
      constructor Create;
      destructor Destroy; override;

     

      procedure AddTexture(Path: String; Texture: TglBitmap2D;
                           OnFinish: TFinishProc = nil);
    protected
      procedure DoTerminate; override; //Hier muss noch das Event gesetzt werden, sondet endet der Thread nicht
      procedure Execute; override;
  end;

[...]
 
procedure TTextureLoader.AddTexture(Path: string; Texture: TglBitmap2D;
                                    OnFinish: TGFinishProc = nil);
// Das Texture-Objekt wird immer schon vor Aufruf dieser Funktion angelegt.
// OnFinish bietet die Möglichkeit, "Bescheid" zu geben, wenn die Textur fertig ist oder nicht geladen werden kann
var
  Job: TJob;
begin
  if FileExists(Path) and Assigned(Texture) then begin
    Job := TJob.Create;
    Job.Path := Path;
    Job.Texture := Texture;
    Job.OnFinish := OnFinish;

    FQueue.Push(Job);

    // Falls Thread angehalten wurde, wieder aufnehmen
    //das macht jetzt die Queue. Du kannst natürlich SetEvent hier aufrufen und generell in die ThreadKlasse legen; weis nicht, was besser ist.
  end;
end;

procedure TTextureLoader.Execute;
begin
  while not Terminated do begin

    // Schlafen legen, wenn keine Jobs mehr vorhanden sind, um den Thread zu erhalten
    if (FQueue.Count = 0) then
      FQueue.Event.Waitfor(infinite); // = waitforsingleobject ohne Zeitbegrenzung
      //hier bei nochmal auf terminated abfragen
    else begin
      //hier evtl. Event.ResetEvent aufrufen

      FCurJob := MTGJob(fQueue.Pop);

      // Eventuell noch ein try..finally rumbasteln, um FCurJob sicher freizugeben und einen Fehler in SyncFailed abzufangen
      try
        FCurJob.Texture.LoadFromFile(FCurJob.Path);
        Synchronize(SyncGenTex);

        Synchronize(SyncSucceeded);
        FCurJob.Free;
      except
        on E: Exception do
          Synchronize(SyncFailed);
      end; // try..except
    end; // if..then..else

  end; // while
end;

procedure TTexturLoader.DoTerminate;
begin
  inherited;
  FQueue.Event.setevent;
end;
Ich denke, ich habe nix vergessen.

Zwoetzen 21. Nov 2008 11:25

Re: [Threads] Textureloader - Einige Fragen
 
Danke, hat mir sehr geholfen. :)

Irgendwie werden nur die Texturen jetzt gar nicht mehr geladen :gruebel: Mir fehlt aber jetzt auch die Zeit, um alles zu prüfen, werde es die nächsten tage nochmal durchgehen ;)


Zu Peek: Neben Pop und Push hat eine Queue auch Peek (oder Top) als Methode, womit man das nächste Element anschauen kann, ohne es direkt mit Pop aus der Queue rauschmeißen zu müssen. Dh das Event muss nicht gefeuert werden, weil nix an der Queue verändert wird ;)
Ich brauche es hier nicht wirklich, habe es nur der Vollständigkeits halber mit hingeschrieben ^^

Zu deinem Kommentar im Execute-Thread:
Wenn er vom Warten zurückkehrt, prüft er doch automatisch durch die umgebende Schleife auf Terminated, sodass eine zusätzliche Prüfung eigentlich nicht nötig sein sollte ;)
Delphi-Quellcode:
procedure TTextureLoader.Execute;
begin
  while not Terminated do begin

    if (FQueue.Count = 0) then
      FQueue.Event.Waitfor(INFINITE);
      // Wenn er zurückkehrt, ist entweder ein neuer Job da, oder der Thread soll beendet werden.
      // Da dies beim nächsten Schleifendurchlauf getestet wird, brauche ich es nicht extra zu behandeln ;)
      // Wobei Terminated Vorrang hat, da zuerst die Schleife, und danach erst die IF-Anweisung geprüft wird
    else begin
      [...]
    end;

  end; // while
end;
____________________
EDIT:

Hab den Fehler gefunden: Wenn man natürlich auch weiterhin den TextureLoader-Thread mit
Delphi-Quellcode:
inherited Create(True);
beim Anlegen suspendiert, kann er ja nie zum Zuge kommen. Kaum macht man's richtig, schon funktioniert's :mrgreen:

Danke nochmals für deine Hilfe, Sirius :thumb:


Alle Zeitangaben in WEZ +1. Es ist jetzt 12:21 Uhr.

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