Application->ProcessMessages Problem



  • Nabend,

    ich habe folgendes vertracktes Problem:

    Ich habe eine MDI Anwendung, die mehrere Formulare gleichzeitig öffnet.
    Eins der Formulare hat einen Timer, bei dessen Triggern Teile einer Datei eingelesen und bearbeitet werden. Da die Datei recht groß ist und die Bearbeitung lange dauert rufe ich Application->ProcessMessages auf, damit die Anwendung nicht steht und noch bearbeitet werden kann. Falls der Benutzer nun aber während des Application->ProcessMessages das Formular schließt, kommt die CPU nach dem Aufruf von Application->ProcessMessages in ein Formular zurück, das bereits freigegeben wurde und damit ein nicht definiertes Verhalten provoziert. Zur Veranschaulichung ein Codeschnipsel (Recordset und TRecord sind eigene Klassen, Details spielen keine Rolle) :

    __fastcall void MyForm::OnTimer( TObject* pSender )
    {
       // Timer für die Dauer der Aktualisierung deaktivieren
       UpdateTimer->Enabled = false;
    
       // Daten aktualisieren
       update();
    
       // Timer wieder aktivieren
       UpdateTimer->Enabled = true;
    }
    
    /** Liest max. 500 Datensätze ab aktueller Dateiposition und aktualisiert die
      * Statistik. Wird alle 500msec durch OnTimer aufgerufen.
     */
    void MyForm::update()
    {
       unsigned int uiRecordCount = 0;
       while( false == Records.eof() && uiRecordCount < 500 )
       {
          // nächsten Datensatz lesen
          TRecord* pRecord = Records.next();
          if( NULL != pRecord )
          {
             // Datensatz behandeln
             handle_record( *pRecord );
          }
          // Zähler für maximale Anzahl von Datensätzen hochzählen
          ++uiRecordCount;
    
          // Windows Nachrichten behandeln
          Application->ProcessMessages();
       }
       // !!!
       // Wenn im obigen Aufruf Application->ProcessMessages() das Formular 
       // geschlossen wird ist das aktuelle Objekt bereits freigegeben und 
       // der this-Pointer ist ungültig. => KABOOM
       // !!!
    
       // Statistik aktualisieren und anzeigen
       update_statistics();
    }
    

    Ich habe bisher folgendes probiert und bin mit meinem Latein am Ende:

    - Einlesen/Aktualisierung durch einen Timer: Problem besteht weiterhin

    - Einlesen Aktualisierung durch eigenen Thread: Da der Thread auf dem Heap angelegt wird bleibt er auch nach dem Freigeben des Formulars bestehen und kann weiterhin Code ausführen. Im Destruktor des Formulars beende ich den Thread per TerminateThread, damit er keine Aktualisierung mehr durchführen kann. Damit habe ich das Hauptproblem gelöst, aber jetzt habe ich Probleme bei der Anzeige der Statistik, da die VCL kein Multithreading unterstützt und ich per Synchronize() den Hauptthread der VCL benutzen muss. Damit bin ich wieder beim alten Problem: KABOOM!
    Selbst wenn ich das Einlesen und Anzeigen trenne (Einlesen durch eigenen Thread, nach dem Einlesen erfolgt eine Benachrichtigung an den VCL Main Thread durch PostMessage()) knallts.

    - eigene ProcessMessages Funktion geschrieben, die mit PeekMessage, GetMessage, TranslateMessage und DispatchMessage die MessageQueue des Formulars und der Anwendung abarbeitet, bis die MessageQueue leer ist oder eine Message vom Typ WM_DESTROY oder WM_CLOSE gefunden wird. Das Problem ist damit gelöst, nur können lediglich zwei Fenster ihre Nachrichten behandeln (das Hauptformular und das aktuelle Formular), wenn ich mit anderen Formularen etwas anstellen will.

    Wenn jemandem was dazu einfällt...

    Gruß,
    Doc



  • Ich glaube nicht, dein Problem richtig verstanden zu haben, aber mal so auf Verdacht: wie wäre es, wenn du das Schliessen des Formulars abfängst (OnCloseQuery), deine Routinen sauber beendest (ggf. mit Bestätigung durch den User) und dann erst das Formular zumachst?



  • Hm, dann versuche ich das mal anders zu beschreiben (habe auch ein wichtiges Detail vergessen):

    Es gibt ein MDI Formular, das einen Frame enthält. Dieser Frame liest zyklisch (timergesteuert) Datensätze ein und bereitet sie auf. Damit der Benutzer während der Datenaufbereitung die Anwendung noch bedienen kann rufe ich nach der Bearbeitung jedes Datensatzes die Methode Application->ProcessMessages auf. Wenn der Benutzer das Formular schliesst und das WM_CLOSE / WM_DESTROY Event durch Application->ProcessMessages behandelt wird passiert folgendes:

    // schematischer Ablauf

    1. Methode MyFrame::update() wird durch Timer aufgerufen
    2. Methode MyFrame::read_records() wird aufgerufen
    3. In der Methode MyFrame::read_records() wird Application->ProcessMessages() aufgerufen
    4. Application->ProcessMessages() behandelt alle Nachrichten in der Message Queue, auch WM_CLOSE / WM_DESTROY. Damit werden vom VCL Framework sowohl das MDI Formular als auch das eingebettete Frame zerstört. Anschliessend kehrt der Aufruf von Application->ProcessMessages() in die aufrufende Methode MyFrame::read_records() zurück. Da aber in Application->ProcessMessages() das Frame zerstört wurde (das Objekt der aufrufenden Methode) ist der Kontext zerstört und read_records erzeugt undefiniertes Verhalten (meistens Absturz). Da hilft auch keine Sicherheitsabfrage oder ähnliches, ich muss Application->ProcessMessages davon abhalten, nach dem Zerstören des Formulars in die read_records() Methode zurückzuspringen. Wahrscheinlich ist mein Design Ansatz falsch und ich muss Einlesen/Aktualisieren voneinander trennen. Mir ist gestern noch was dazu eingefallen, werde das jetzt mal testen.
      Trotzdem Danke für deinen Tipp.

    Gruß,
    Doc



  • deine idee mit dem thread war schon nicht falsch, aber statt den Thread per synchronize in die GUI schreiben zu lassen, könntest du doch 2 events machen, 1 event löst den thread aus einem.
    waitforsingleobject(Ev1,INFINITE);

    der thread bearbeitet die datei und meldet mit einem event Ev2 an die gui zurück das er fertig ist, in der GUI prüfst du vor dem setzen des Ev1 ob Ev2 gesetzt ist, ist dem so aktualisierst du die GUI mit den in der Threadklasse liegenden daten, und löst den thread erneut aus.

    ist Ev2 bei erneutem start des timer NICHT gesetzt, solltest du den thread auf hangup überprüfen oder eventuell den timer heraufsetzen

    nochmal EDIT: allgemein würd ich von der verwendung des processMessages() abraten, stackaufrufe enden nur in einer katastrophe >_< wenn man nicht peinlich genau aufpasst wer was wo wann und wie aufruft



  • So, mal ausgeschlafen nachgedacht und Problem gelöst:

    Strikte Trennung zwischen Einlesen/Bearbeitung und Anzeige der Daten. Ich habe in meinem Frame jetzt einen Thread auf niedrigster Priorität, der Daten einliest und aufbereitet. Er läuft in einer Endlosschleife, liest max. 500 Datensätze ein, legt sich 50 msec schlafen und wiederholt das, bis er an das Dateiende gekommen ist. Das Frame selbst hat jetzt einen Timer, der jede Sekunde die Darstellung aktualisiert. Im Destruktor des Frames wird der zugehörige Thread beendet, da er den Frame nicht mehr mit Daten beliefern kann.
    Damit brauche ich ProcessMessages() nicht mehr und alles ist gut.

    Vielen Dank für eure Hilfe,
    Doc


Anmelden zum Antworten