Delphi-PRAXiS
Seite 1 von 2  1 2      

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Programmieren allgemein (https://www.delphipraxis.net/40-programmieren-allgemein/)
-   -   Rechenintensiven Thread aufräumen (https://www.delphipraxis.net/182668-rechenintensiven-thread-aufraeumen.html)

Schwedenbitter 9. Nov 2014 12:00

Rechenintensiven Thread aufräumen
 
Ich habe ein Scanprogramm, dass die Bilder sofort verarbeitet und dem Benutzer Vorschläge für die beste Komprimierung machen soll. Für die Berechnung werden Threads verwendet. Das Komprimieren der Bilder (derzeit PNG und JPEG) erledige ich über den Code/die Units von Delphi. Und weil die Bilder größer sind, kann es manchmal dauern.
Wenn der Benutzer das Programm beendet, klappt das Dank Threads, hinterlässt aber Speicherlecks. Das gefällt mir natürlich nicht.

Der Code dazu sieht folgendermaßen aus:
Delphi-Quellcode:
Unit _CalcThread;

Interface

Uses
   System.Classes, Vcl.Graphics, Vcl.Imaging.pngimage, Vcl.Imaging.jpeg;

Type
   TCodecType   = (cdPNG, cdJPEG);
   TOnCalcDone   = Procedure(Codec: TCodecType; Size: Int64) Of Object;
   TCalcThread   = Class(TThread)
                 Private
                     fSize         : Int64;
                     fCodec      : TCodecType;
                     fBitmap      : TBitmap;
                     fOnCalcDone   : TOnCalcDone;
                     Procedure EventOnCalcDone;
                 Public
                     Constructor Create(Codec: TCodecType; Bitmap: TBitmap;
                        OnCalcDone: TOnCalcDone);
                     Procedure Execute; Override;
                     Destructor Destroy; Override;
                 End;

Implementation

// Absicherung des Eriegnis-Aufrufs --------------------------------------------
Procedure TCalcThread.EventOnCalcDone;
Begin
   If Assigned(fOnCalcDone) Then                     // Sicherheitsprüfung
      FOnCalcDone(fCodec, fSize);                  // Ereignis auslösen
End;

// Ein Thread wird erstellt ----------------------------------------------------
Constructor TCalcThread.Create(Codec: TCodecType; Bitmap: TBitmap;
   OnCalcDone: TOnCalcDone);
Begin
   fSize:=     -1;                                 // ungültigen Wert vorgeben
   fCodec:=    Codec;                              // Codec merken
   fOnCalcDone:=OnCalcDone;                        // Zeiger auf Ereignis merken
   fBitmap:=TBitmap.Create;                        // TBitmap anlegen
   fBitmap.Assign(Bitmap);                           // Bild kopieren
   FreeOnTerminate:=True;                           // Speicher selbst freigeben

   Inherited Create(True);                           // Thread erstellen
   Priority:=      tpIdle;                        // geringe Priorität
//   Resume;                                          // "Start" löst Exception aus!
   Start;                                       // "Resume" ist veraltet
End;

// Der eigentliche Thread (=die Berechnung) ------------------------------------
Procedure TCalcThread.Execute;
Var
   lMemStream      : TMemoryStream;
Begin
   lMemStream:=TMemoryStream.Create;               // Nur im Speicher arbeiten
   Try
      Case fCodec Of
         cdPNG   :   With TPngImage.Create Do         // temp. PNG-Objekt anlegen
                  Try
                     Filters:=[pfNone, pfSub, pfUp, pfAverage, pfPaeth];
                     CompressionLevel:=9;            // Maximale Kompression
                     Assign(fBitmap);               // Bilddaten übernehmen
                     SaveToStream(lMemStream);      // virtuell abspeichern
                     fSize:=lMemStream.Size;         // Größe merken
                  Finally
                     Free;                           // PNG-Objekt freigeben
                  End;
         cdJPEG:   With TJPEGImage.Create Do         // tenp. JPEG-Objekt anlegen
                  Try
                     CompressionQuality:=80;         // 100 = max, 1 = min
                     Assign(fBitmap);               // Bilddaten übernehmen
                     SaveToStream(lMemStream);      // virtuell abspeichern
                     fSize:=lMemStream.Size;         // Größe merken
                  Finally
                     Free;                           // JPEG-Objekt freigeben
                  End;
      End;
   Finally
      lMemStream.Free;                              // TMemoryStream freigeben
      Synchronize(EventOnCalcDone);                  // Das Ende mitteilen
   End;
End;

// Der Thread wird beendet -----------------------------------------------------
Destructor TCalcThread.Destroy;
Begin
   fBitmap.Free;                                    // TBitmap selbst freigeben
   fOnCalcDone:=nil;                                 // Zeiger löschen
   Inherited;                                       // den Rest ausführen
End;

End.
Da der Code unter
Delphi-Quellcode:
Execute
sehr kurz ist, macht es keinen Sinn auf
Delphi-Quellcode:
Terminated
zu reagieren.

Zusatzfrage:
Der Compiler sowie die Hilfe sagen mir,
Delphi-Quellcode:
TThread.Resume;
sei veraltet. Allerdings erzeugt
Delphi-Quellcode:
TThread.Start;
bei mir immer eine Exception vom Typ
Delphi-Quellcode:
EInvalidPointer
. Und was noch schlimmer ist: bevor ich die Message dazu erhalte, zeigt mir Windows 8.1 an "Das Programm ... funktioniert nicht mehr richtig. Online nach einer Lösung suchen...". Der Debugger stürzt ab, die exe-Datei lässt sich nicht mehr löschen und damit neu compilieren.

Wie löse ich diese Probleme?

Gruß, Alex

jfheins 9. Nov 2014 12:11

AW: Rechenintensiven Thread aufräumen
 
Ich würde es bei den Speicherlecks lassen - Windows räumt ja auf. Aber wenn es dir so wichtig ist:

:arrow: Komprimiere nur einen kleinen Teil des Bilds und nehme dies als Anhaltspunkt, welches Format besser ist
:arrow: Schreibe den JPG/PNG Algorithmus selbst oder binde eine Bibliothek ein, die das unterbrechen gestattet.
:arrow: Entwickle eine Heuristik. Zum Beispiel sind Bilder mit Gradienten und homogenen Flächen tendenziell besser für PNG geeignet, Bilder mit vielen Details besser für JPG.

BUG 9. Nov 2014 12:42

AW: Rechenintensiven Thread aufräumen
 
Halt, warte: Du beendest das Programm während die Threads noch laufen und wunderst dich dann das es Speicherlecks gibt? Wenn du die Threads nicht "sauber" beendest, wird der Speicher nicht freigegeben.

Hier gibt es die Variante, eine Exception in einem Thread zu erzeugen. Nicht wirklich sauber, aber ziemlich clever.

Eine etwas saubere Lösung wäre es, dir ein TStream-Delegate zu erstellen, mit dem du die Berechnung aus einem anderem Thread stoppen kannst (durch Exceptions und/oder fehlgeschlagene Writes).


Was mich ein wenig wundert, ist dass du für die "Abschätzung" die eigentliche Berechnung ausführst und dann das Ergebnis wegschmeißt. Wenn der Benutzer sich dann entschieden hat, machst du die Kompression nochmal?

Zitat:

Zitat von Schwedenbitter (Beitrag 1279198)
Der Compiler sowie die Hilfe sagen mir,
Delphi-Quellcode:
TThread.Resume;
sei veraltet. Allerdings erzeugt
Delphi-Quellcode:
TThread.Start;
bei mir immer eine Exception vom Typ
Delphi-Quellcode:
EInvalidPointer

Erstellte den Thread nicht suspended! Der Konstruktor wird immer in dem Kontext des erstellenden Threads ausgeführt. Das Start ist damit völlig unnötig.

himitsu 9. Nov 2014 13:14

AW: Rechenintensiven Thread aufräumen
 
Wie wäre es, wenn man auf das Ende der Threads wartet, bevor sich die Anwendung beendet? :stupid:

Schwedenbitter 9. Nov 2014 14:29

AW: Rechenintensiven Thread aufräumen
 
Zitat:

Zitat von himitsu (Beitrag 1279211)
Wie wäre es, wenn man auf das Ende der Threads wartet, bevor sich die Anwendung beendet? :stupid:

So mache ich es im Moment. Aber das Komprimieren je einer JPEG und einer PNG-Datei kann bei einem 200 dpi Bild mit durchschnittlicher CPU mal eben jeweils 8 Sekunden dauern. Bei 10 Bildern will man nicht wirklich warten. Mir wäre es auch egal, weil der Benutzer die Speicherlecks nicht sieht. Aber es ist unsaubere Programmierung.

himitsu 9. Nov 2014 14:53

AW: Rechenintensiven Thread aufräumen
 
  • die Thread-Instanz darf sich nicht selber freigeben
  • das Programm ruft Thread.Terminate auf
  • die Threads beenden sich (nach durchschnittlich 4 Sekunden aka 50% von 8 Sekunden)
  • das Programm wartet auf das Ende
  • und beendet sich dann

BUG 9. Nov 2014 15:01

AW: Rechenintensiven Thread aufräumen
 
Zitat:

Zitat von Schwedenbitter (Beitrag 1279219)
Aber das Komprimieren je einer JPEG und einer PNG-Datei kann bei einem 200 dpi Bild mit durchschnittlicher CPU mal eben jeweils 8 Sekunden dauern.

Nochmal: Du schmeißt 8 Sekunden Arbeitszeit einfach weg, wenn du die Größe hast :shock:

Zitat:

Zitat von Schwedenbitter (Beitrag 1279219)
Bei 10 Bildern will man nicht wirklich warten.

Eventuell könnte dir ein Threadpool gut tun. Wenn die 10 Threads nebenläufig rödeln, kommen die sich vermutlich bloß in die Quere.
Außerdem könntest du im Threadpool vor dem Vergeben einer neuen Aufgabe prüfen, ob die Anwendung beendet wurde.

An deiner Stelle würde ich mir tatsächlich eine TStream-Ableitung schreiben und mal gucken, wie die Bibliotheken den Stream benutzen. Wenn die regelmäßig Schreibzugriffe auf den Stream machen, dann wäre das eine schöne Gelegenheit, den ganzen Quark mit einer Exception (oÄ.) abzubrechen.

Luckie 10. Nov 2014 00:28

AW: Rechenintensiven Thread aufräumen
 
Wenn ein Prozess beendet wird, egal wie, ob regulär oder wenn über den Taskmanager, gibt es keine Speicherlecks. Wo sollen die auch sein? Speicherlecks kann es nur innerhalb eines Prozesses geben. Dieser existiert aber nicht mehr. Man kann ja auch kein Loch in einem Loch buddeln. ;) Des weiteren schließt Windows automatisch alle von dem Prozess geöffneten Händels. Also: Prozess beendet, alles gut.

Natürlich kann man darüber diskutieren, ob es guter Programmierstil ist nicht hinter sich aufzuräumen. Da hat jeder seine eigene Meinung.

himitsu 10. Nov 2014 08:45

AW: Rechenintensiven Thread aufräumen
 
Wenn man selber aufräumt, dann sieht man zumindestens, ob sich unbeabsichte Löcher verstecken.
Und wenn richtig aufgeräumt wird, dann sind Fehler beim Beenden minimiert.

z.B. Komponente auf Form, die beim Beenden auf etwas Globales in einer Unit zugreift ... wenn die Unit bereits ordentlich entladen wurde, dann würde es durt womöglich schön knallen.

Sir Rufo 10. Nov 2014 11:08

AW: Rechenintensiven Thread aufräumen
 
Die Spiecherlecks werdn doch deshalb angezeigt, weil die Threads beim Beenden des Prozesses einfach abgeschossen werden und die von jedem Thread belegten Ressourcen nicht freigeben wurden.

Wenn man das nicht haben möchte, dann muss man sich eben selber um die Freigabe der Threads kümmern und nicht
Delphi-Quellcode:
TThread.FreeOnTerminate := True;
wegdelegieren.

BTW: Das hier ist grober Unfug:
Delphi-Quellcode:
Constructor TCalcThread.Create(Codec: TCodecType; Bitmap: TBitmap;
   OnCalcDone: TOnCalcDone);
Begin
   fSize:= -1;                                // ungültigen Wert vorgeben
   fCodec:= Codec;                             // Codec merken
   fOnCalcDone:=OnCalcDone;                       // Zeiger auf Ereignis merken
   fBitmap:=TBitmap.Create;                       // TBitmap anlegen
   fBitmap.Assign(Bitmap);                          // Bild kopieren
   FreeOnTerminate:=True;                          // Speicher selbst freigeben

   Inherited Create(True);                          // Thread erstellen
   Priority:= tpIdle;                       // geringe Priorität
//   Resume;                                         // "Start" löst Exception aus!
   Start;                                      // "Resume" ist veraltet
End;
Während der
Delphi-Quellcode:
Constructor
abgearbeitet wird wird der Thread nicht loslaufen! Mit der Erkenntnis kann der Code wie folgt geschreiben werden:
Delphi-Quellcode:
Constructor TCalcThread.Create(Codec: TCodecType; Bitmap: TBitmap;
   OnCalcDone: TOnCalcDone);
Begin
   fSize := -1;              // ungültigen Wert vorgeben
   fCodec := Codec;          // Codec merken
   fOnCalcDone := OnCalcDone; // Zeiger auf Ereignis merken
   fBitmap := TBitmap.Create; // TBitmap anlegen
   fBitmap.Assign( Bitmap ); // Bild kopieren

   Inherited Create( False ); // Thread erstellen
   Priority := tpIdle;       // geringe Priorität
   FreeOnTerminate := True;  // Instanz selbst freigeben <- hmmm, nicht geschickt
End;
Der
Delphi-Quellcode:
Destructor
sollte allerdings wie folgt aufgebaut werden:
Delphi-Quellcode:
Destructor TCalcThread.Destroy;
Begin
   fOnCalcDone:=nil;         // Zeiger löschen

   Inherited;                // den Rest ausführen

   fBitmap.Free;             // TBitmap selbst freigeben
End;
In
Delphi-Quellcode:
TThread.Destroy
wird unter anderem
Delphi-Quellcode:
TThread.Terminate
aufgerufen, weil der Thread ja noch aktiv sein kann. Wenn man dem Thread während der Abarbeitung die Ressource fBitmap unter dem A.... wegzieht, was kann dann passieren? Genau, es knallt. Also erst
Delphi-Quellcode:
inherited
, danach ist der Thread gesichert beendet und dann können alle Ressourcen ohne Reue freigeben werden!


Alle Zeitangaben in WEZ +1. Es ist jetzt 20:26 Uhr.
Seite 1 von 2  1 2      

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