Ich habe festgestellt, dass, wenn eine mit TThread.Queue
in die Warteschlange eingereihte Methode eine Methode aufruft, die TApplication.WndProc
aufruft (z. B. ShowMessage
), nachfolgende in der Warteschlange stehende Methoden dürfen ausgeführt werden, bevor die ursprüngliche Methode abgeschlossen wurde. Schlimmer noch, sie scheinen nicht in FIFO-Reihenfolge aufgerufen zu werden.
[Bearbeiten: Tatsächlich beginnen sie in FIFO-Reihenfolge. Mit ShowMessage
sieht es so aus, als würde der spätere zuerst ausgeführt, da ein Aufruf von CheckSynchronize
vor dem Erscheinen des Dialogs erfolgt. Dies hebt die nächste Methode auf und führt sie aus, wobei sie erst zurückkehrt, wenn die letztere Methode abgeschlossen ist. Erst dann erscheint der Dialog.]
Ich versuche sicherzustellen, dass alle Methoden, die aus dem Worker-Thread in den VCL-Thread eingereiht wurden, in strikter FIFO-Reihenfolge ausgeführt werden und dass sie alle abgeschlossen sind, bevor der Worker-Thread zerstört wird.
Meine andere Einschränkung besteht darin, dass ich versuche, die GUI strikt von der Geschäftslogik zu trennen. Der Thread ist in diesem Fall Teil des Geschäftslogik-Layers, daher kann ich PostMessage
von einem OnTerminate
-Handler nicht verwenden, um dafür zu sorgen, dass der Thread zerstört wird (wie von einer Reihe von Mitwirkenden an anderer Stelle empfohlen). Also setze ich FreeOnTerminate := True
in einer letzten eingereihten Methode, kurz bevor TThread.Execute beendet wird. (Daher müssen sie in einer strengen FIFO-Reihenfolge ausgeführt werden.)
So endet meine TThread.Execute-Methode:
%Vor%Aber wie gesagt, das funktioniert nicht, weil diese in die Warteschlange gestellte Methode vor einigen der früheren Methoden in der Warteschlange ausgeführt wird.
Kann jemand einen einfachen und sauberen Weg vorschlagen, um das zu schaffen, oder eine einfache und saubere Alternative?
[Die einzige Idee, die ich bis jetzt entwickelt habe und die Trennung von Bedenken und Modularität beibehält, besteht darin, meiner TThread
-Unterklasse ein WndProc
zu geben. Dann kann ich PostMessage
direkt zu diesem WndProc anstelle des Hauptformulars verwenden. Aber ich hoffe auf etwas Leichtes.]
Danke für die Antworten und Kommentare bis jetzt. Ich verstehe jetzt, dass mein Code oben mit einem eingereihten SetEvent
und WaitForSingleObject
funktional dem Aufruf von Synchronize
am Ende entspricht anstatt von Queue
, weil Queue
und Synchronize
die gleiche Warteschlange teilen. Ich habe Synchronize
zuerst ausprobiert und dies ist aus dem gleichen Grund fehlgeschlagen, wie der obige Code fehlschlägt - die früher eingereihten Methoden rufen die Nachrichtenbehandlung auf, sodass die letzte Synchronize
-Methode ausgeführt wird, bevor die zuvor eingereihten Methoden abgeschlossen wurden.
Ich bin also immer noch an das ursprüngliche Problem gebunden, das nun auf Folgendes hinausläuft: Kann ich sauber sicherstellen, dass alle in der Warteschlange befindlichen Methoden abgeschlossen sind, bevor der Worker-Thread freigegeben wird, und den Worker-Thread sauber freigeben kann, ohne% zu verwenden? co_de%, für das ein Fenster-Handle benötigt wird, auf das gebucht werden soll (auf das meine Business-Schicht keinen Zugriff hat).
Ich habe den Titel auch besser aktualisiert, um das ursprüngliche Problem widerzuspiegeln, obwohl ich mich für eine alternative Lösung freuen würde, die PostMessage
gegebenenfalls nicht verwendet. Wenn sich jemand einen besseren Titel ausdenken kann, dann bitte editieren.
Ein weiteres Update: Diese Antwort von David Heffernan empfiehlt die Verwendung von TThread.Queue
mit einem speziellen PostMessage
im allgemeinen Fall wenn AllocateHWnd
nicht verfügbar oder geeignet ist. Bezeichnenderweise ist es nie sicher, TThread.Queue
für das Hauptformular zu verwenden, da das Fenster spontan neu erstellt werden kann, wenn sein Handle geändert wird, wodurch alle nachfolgenden Nachrichten an das alte Handle verloren gehen würden. Dies ist ein starkes Argument für mich, diese spezielle Lösung zu übernehmen, da es keinen zusätzlichen Aufwand für das Erstellen eines versteckten Fensters in meinem Fall gibt, da any -Anwendung das PostMessage
verwenden soll - also mein Argument "Trennung von Interessen" irrelevant.
Dies ist die Lösung, die ich schließlich angenommen habe.
Ich habe Delphi TCountdownEvent
verwendet, um die Anzahl der ausstehenden Methoden in der Warteschlange zu verfolgen aus meinem Thread, inkrementieren die Zählung kurz vor dem Einreihen einer Methode, und dekrementieren es als der letzte Akt der in die Warteschlange eingereihten Methode.
Kurz bevor mein Überschreiben von TThread.Execute
zurückkehrt, wartet es darauf, dass das TCountdownEvent
-Objekt signalisiert wird, d. h. wenn der Zählwert Null erreicht. Dies ist der entscheidende Schritt, der garantiert, dass alle in die Warteschlange gestellten Methoden abgeschlossen sind, bevor Execute
zurückgibt.
Sobald alle in der Warteschlange stehenden Methoden abgeschlossen sind, ruft sie Synchronize
mit einem OnComplete
-Handler auf - dank Remy, um darauf hinzuweisen, dass dies äquivalent zu, aber einfacher als mein ursprünglicher Code ist, der Queue
und WaitForSingleObject
verwendet . ( OnComplete
ist wie OnTerminate
, wird aber vor Execute aufgerufen, damit der Handler FreeOnTerminate
ändern kann.)
Die einzige Falte ist, dass TCountdownEvent.AddCount
nur funktioniert, wenn die Anzahl bereits größer als Null ist. Also habe ich einen Klassenhelfer geschrieben, um ForceAddCount
zu implementieren:
Normalerweise wäre das riskant, aber in meinem Fall wissen wir, dass zu dem Zeitpunkt, an dem der Thread auf die Anzahl der ausstehenden Methoden wartet, um Null zu erreichen, keine weiteren Methoden mehr in die Warteschlange gestellt werden können bleib bei Null).
Dies löst nicht vollständig das Problem von eingereihten Methoden, die Nachrichten verarbeiten, da einzelne eingereihte Methoden immer noch nicht ordnungsgemäß funktionieren. Aber ich habe jetzt die Garantie, dass alle in der Warteschlange befindlichen Methoden asynchron ausgeführt werden, aber abgeschlossen sein werden, bevor der Thread beendet wird. Dies war das primäre Ziel, da es dem Thread ermöglichte, sich selbst aufzuräumen, ohne das Risiko zu haben, in die Warteschlange gestellte Methoden zu verlieren.
TThread.Queue()
ist eine FIFO-Warteschlange. Tatsächlich teilt es dieselbe Warteschlange, die Thread.Sychronize()
verwendet. Aber Sie haben Recht, dass die Nachrichtenbehandlung dazu führt, dass in der Warteschlange befindliche Methoden ausgeführt werden. Dies liegt daran, dass TApplication.Idle()
Aufrufe von CheckSynchronize()
immer dann aufruft, wenn die Nachrichtenwarteschlange nach der Verarbeitung neuer Nachrichten inaktiv wird. Wenn also eine eingereihte / synchronisierte Methode die Nachrichtenverarbeitung aufruft, können andere in der Warteschlange stehende / synchronisierte Methoden ausgeführt werden, selbst wenn die frühere Methode noch ausgeführt wird.
Wenn Sie sicherstellen möchten, dass eine Warteschlangenmethode aufgerufen wird, bevor der Thread beendet wird, sollten Sie Synchronize()
anstelle von Queue()
verwenden oder stattdessen das Ereignis OnTerminate
verwenden (das von Synchronize()
ausgelöst wird). Was Sie in Ihrem finally
-Block tun, ist eigentlich das Gleiche wie das OnTerminate
-Ereignis bereits nativ tut.
Das Festlegen von FreeOnTerminate := True
in einer eingereihten -Methode verlangt nach einem Speicherverlust. FreeOnTerminate
wird sofort ausgewertet, wenn Execute()
beendet wird, bevor DoTerminate()
aufgerufen wird, um das OnTerminate
-Ereignis auszulösen (was meiner Meinung nach ein Versehen ist, wenn es so früh bewertet wird, dass OnTerminate
nicht zum Zeitpunkt der Beendigung entscheidet, ob a Thread sollte sich selbst freigeben oder nicht, nachdem OnTerminate
beendet wurde). Wenn also die Methode in der Warteschlange ausgeführt wird, nachdem Execute()
beendet wurde, gibt es keine Garantie dafür, dass FreeOnTerminate
rechtzeitig gesetzt wird. Das Warten auf eine queued -Methode, die abgeschlossen werden muss, bevor die Steuerung an den Thread zurückgegeben wird, ist genau das, für das Synchronize()
bestimmt ist. Synchronize()
ist synchron und wartet auf das Beenden der Methode. Queue()
ist asynchron, es wartet überhaupt nicht.
Ich habe dieses Problem behoben, indem ich am Ende meiner Synchronize()
-Methode einen Aufruf von Execute()
hinzugefügt habe. Dies zwingt den Thread zu warten, bis alle Aufrufe, die mit Queue()
hinzugefügt wurden, im Hauptthread abgeschlossen sind, bevor der mit Synchronize()
hinzugefügte Aufruf aufgerufen werden kann.
Ein paar Gedanken:
Tags und Links multithreading delphi