Thread-Klasse in Delphi
Raptor[Mental Studio]
http://mental.mentsu.com
Vier
Critical Section (CriticalSection) ist eine Technologie zum gemeinsamen Schutz des Datenzugriffs. Es entspricht tatsächlich einer globalen booleschen Variablen. Seine Funktionsweise ist jedoch unterschiedlich. Es gibt nur zwei Operationen: Eingeben und Verlassen. Seine beiden Zustände können auch als wahr und falsch angesehen werden, was angibt, ob es sich im kritischen Abschnitt befindet. Diese beiden Operationen sind ebenfalls Grundfunktionen und können daher zum Schutz gemeinsam genutzter Daten vor Zugriffsverletzungen in Multithread-Anwendungen verwendet werden.
Die Methode, kritische Abschnitte zum Schutz gemeinsam genutzter Daten zu verwenden, ist sehr einfach: Rufen Sie Enter auf, um vor jedem Zugriff auf gemeinsam genutzte Daten das Flag für kritische Abschnitte zu setzen, bearbeiten Sie dann die Daten und rufen Sie schließlich Leave auf, um den kritischen Abschnitt zu verlassen. Das Schutzprinzip lautet wie folgt: Nachdem ein Thread den kritischen Abschnitt betreten hat und zu diesem Zeitpunkt auch ein anderer Thread auf die Daten zugreifen möchte, wird beim Aufrufen von Enter festgestellt, dass ein Thread bereits in den kritischen Abschnitt eingetreten ist, und dieser Thread wird dies tun Legen Sie auf und warten Sie, bis der Thread, der sich derzeit im kritischen Abschnitt befindet, „Leave“ aufruft, um den kritischen Abschnitt zu verlassen. Wenn ein anderer Thread den Vorgang abschließt und „Leave“ aufruft, um ihn zu verlassen, wird dieser Thread aktiviert, das Flag für den kritischen Abschnitt gesetzt und mit dem Betrieb der Daten begonnen. und verhindert so den Zugriff.
Am Beispiel des vorherigen InterlockedInkrementes verwenden wir CriticalSection (Windows-API), um es zu implementieren:
Var
InterlockedCrit : TRTLCriticalSection;
PROcedure InterlockedIncrement( var aValue : Integer );
Beginnen
EnterCriticalSection(InterlockedCrit);
Inc(aValue);
LeaveCriticalSection(InterlockedCrit);
Ende;
Schauen Sie sich nun das vorherige Beispiel an:
1. Thread A betritt den kritischen Abschnitt (vorausgesetzt, die Daten sind 3)
2. Thread B betritt den kritischen Abschnitt, da sich A bereits im kritischen Abschnitt befindet.
3. Thread A fügt eins zu den Daten hinzu (jetzt 4)
4. Thread A verlässt den kritischen Abschnitt und weckt Thread B auf (die aktuellen Daten im Speicher sind 4).
5. Thread B wacht auf und fügt eins zu den Daten hinzu (jetzt sind es 5).
6. Thread B verlässt den kritischen Abschnitt und die aktuellen Daten sind korrekt.
Auf diese Weise schützen kritische Abschnitte den Zugriff auf gemeinsam genutzte Daten.
Bei der Verwendung kritischer Abschnitte ist die Behandlung von Ausnahmen beim Datenzugriff zu beachten. Denn wenn während der Datenoperation eine Ausnahme auftritt, wird die Leave-Operation nicht ausgeführt. Infolgedessen wird der Thread, der aktiviert werden sollte, nicht aktiviert, was dazu führen kann, dass das Programm nicht mehr reagiert. Im Allgemeinen besteht der richtige Ansatz darin, kritische Abschnitte wie folgt zu verwenden:
Geben Sie CriticalSection ein
Versuchen
//Kritische Abschnittsdaten verarbeiten
Endlich
LeaveCriticalSection
Ende;
Als letztes ist zu beachten, dass Event und CriticalSection beide Betriebssystemressourcen sind, die vor der Verwendung erstellt und nach der Verwendung freigegeben werden müssen. Beispielsweise werden ein globales Event: SyncEvent und ein globaler CriticalSection: TheadLock, die von der TThread-Klasse verwendet werden, beide in InitThreadSynchronization und DoneThreadSynchronization erstellt und freigegeben, und sie werden in der Einheit „Initialisierung und Finalisierung der Klassen“ aufgerufen.
Da APIs zum Betrieb von Event und CriticalSection in TThread verwendet werden, wird die API oben als Beispiel verwendet. Tatsächlich hat Delphi eine Kapselung derselben in der SyncObjs-Einheit bereitgestellt. Die Verwendung ist fast die gleiche wie bei der vorherigen Methode zur Verwendung der API. Da der Konstruktor von TEvent zu viele Parameter hat, stellt Delphi der Einfachheit halber auch eine Event-Klasse bereit, die mit Standardparametern initialisiert ist: TSimpleEvent.
Lassen Sie mich übrigens eine weitere Klasse vorstellen, die für die Thread-Synchronisierung verwendet wird: TMultiReadExclusiveWriteSynchronizer, die in der SysUtils-Unit definiert ist. Soweit ich weiß, ist dies der längste in Delphi RTL definierte Klassenname. Glücklicherweise gibt es einen kurzen Alias: TMREWSync. Was die Verwendung betrifft, denke ich, dass man sie allein anhand des Namens erkennen kann, deshalb werde ich nicht mehr sagen.
Mit den bisherigen vorbereitenden Kenntnissen über Event und CriticalSection können wir offiziell mit der Diskussion von Synchronize und WaitFor beginnen.
Wir wissen, dass Synchronize die Thread-Synchronisierung dadurch erreicht, dass ein Teil des Codes zur Ausführung im Haupt-Thread platziert wird, da es in einem Prozess nur einen Haupt-Thread gibt. Schauen wir uns zunächst die Implementierung von Synchronize an:
procedure TThread.Synchronize(Method: TThreadMethod);
beginnen
FSynchronize.FThread := Self;
FSynchronize.FSynchronizeException := nil;
FSynchronize.FMethod := Methode;
Synchronisieren(@FSynchronize);
Ende;
wobei FSynchronize ein Datensatztyp ist:
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = Datensatz
FThread: TObject;
FMethod: TThreadMethod;
FSynchronizeException: TObject;
Ende;
Wird für den Datenaustausch zwischen Threads und dem Hauptthread verwendet, einschließlich eingehender Thread-Klassenobjekte, Synchronisationsmethoden und auftretender Ausnahmen.
Eine überladene Version davon wird in Synchronize aufgerufen, und diese überladene Version ist etwas ganz Besonderes, es handelt sich um eine „Klassenmethode“. Die sogenannte Klassenmethode ist eine spezielle Klassenmitgliedsmethode. Ihr Aufruf erfordert nicht die Erstellung einer Klasseninstanz, sondern wird wie ein Konstruktor über den Klassennamen aufgerufen. Der Grund für die Implementierung mithilfe einer Klassenmethode besteht darin, dass sie auch dann aufgerufen werden kann, wenn das Thread-Objekt nicht erstellt wurde. In der Praxis werden jedoch eine andere überladene Version davon (ebenfalls eine Klassenmethode) und eine andere Klassenmethode StaticSynchronize verwendet. Hier ist der Code für diese Synchronisierung:
Klassenprozedur TThread.Synchronize(ASyncRec: PSynchronizeRecord);
var
SyncProc: TSyncProc;
beginnen
wenn GetCurrentThreadID = MainThreadID dann
ASyncRec.FMethod
anders
beginnen
SyncProc.Signal := CreateEvent(nil, True, False, nil);
versuchen
EnterCriticalSection(ThreadLock);
versuchen
Wenn SyncList = Null, dann
SyncList := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
SignalSyncEvent;
wenn Assigned(WakeMainThread), dann
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection(ThreadLock);
versuchen
WaitForSingleObject(SyncProc.Signal, INFINITE);
Endlich
EnterCriticalSection(ThreadLock);
Ende;
Endlich
LeaveCriticalSection(ThreadLock);
Ende;
Endlich
CloseHandle(SyncProc.Signal);
Ende;
wenn Assigned(ASyncRec.FSynchronizeException) dann ASyncRec.FSynchronizeException auslösen;
Ende;
Ende;
Dieser Code ist etwas länger, aber nicht zu kompliziert.
Die erste besteht darin, festzustellen, ob der aktuelle Thread der Hauptthread ist. Wenn ja, führen Sie einfach die Synchronisierungsmethode aus und kehren Sie zurück.
Wenn es sich nicht um den Hauptthread handelt, ist er bereit, den Synchronisierungsprozess zu starten.
Die Thread-Austauschdaten (Parameter) und ein Ereignishandle werden über die lokale Variable SyncProc aufgezeichnet. Die Datensatzstruktur ist wie folgt:
TSyncProc=Aufzeichnung
SyncRec: PSynchronizeRecord;
Signal: THandle;
Ende;
Erstellen Sie dann ein Ereignis, geben Sie dann den kritischen Abschnitt ein (über die globale Variable ThreadLock, da nur ein Thread gleichzeitig in den Synchronisierungsstatus wechseln kann, sodass Sie die globale Variable zum Aufzeichnen verwenden können) und speichern Sie dann die aufgezeichneten Daten im SyncList-Liste (falls diese Liste nicht vorhanden ist, erstellen Sie sie). Es ist ersichtlich, dass der kritische Abschnitt von ThreadLock darin besteht, den Zugriff auf SyncList zu schützen. Dies wird später bei der Einführung von CheckSynchronize erneut sichtbar.
Der nächste Schritt besteht darin, SignalSyncEvent aufzurufen. Sein Code wurde bereits bei der Einführung des TThread-Konstruktors eingeführt. Seine Funktion besteht einfach darin, eine Set-Operation für SyncEvent auszuführen. Der Zweck dieses SyncEvents wird später bei der Einführung von WaitFor detailliert beschrieben.
Als nächstes kommt der wichtigste Teil: das Aufrufen des WakeMainThread-Ereignisses für Synchronisierungsvorgänge. WakeMainThread ist ein globales Ereignis vom Typ TNotifyEvent. Der Grund, warum hier Ereignisse für die Verarbeitung verwendet werden, liegt darin, dass die Synchronize-Methode den Prozess, der synchronisiert werden muss, im Wesentlichen in den Hauptthread versetzt, um ihn über Nachrichten auszuführen. Sie kann in einigen Anwendungen ohne Nachrichtenschleifen (z. B. Konsole oder DLL) nicht verwendet werden. , also nutzen Sie dieses Ereignis zur Verarbeitung.
Das Anwendungsobjekt reagiert auf dieses Ereignis. Die folgenden zwei Methoden werden verwendet, um die Antwort auf das WakeMainThread-Ereignis (von der Forms-Einheit) festzulegen und zu löschen:
procedure TApplication.HookSynchronizeWakeup;
beginnen
Classes.WakeMainThread := WakeMainThread;
Ende;
procedure TApplication.UnhookSynchronizeWakeup;
beginnen
Classes.WakeMainThread := nil;
Ende;
Die beiden oben genannten Methoden werden im Konstruktor bzw. Destruktor der TApplication-Klasse aufgerufen.
Dies ist der Code, der auf das WakeMainThread-Ereignis im Anwendungsobjekt reagiert. Die Nachricht wird hier gesendet. Dazu wird eine leere Nachricht verwendet:
procedure TApplication.WakeMainThread(Sender: TObject);
beginnen
PostMessage(Handle, WM_NULL, 0, 0);
Ende;
Die Antwort auf diese Nachricht befindet sich auch im Anwendungsobjekt, siehe folgenden Code (irrelevante Teile entfernen):
procedure TApplication.WndProc(var Message: TMessage);
…
beginnen
versuchen
…
mit Nachricht tun
Fall Nachricht von
…
WM_NULL:
CheckSynchronize;
…
außer
HandleException(Self);
Ende;
Ende;
Unter diesen ist auch CheckSynchronize in der Klasseneinheit definiert. Da es relativ komplex ist, werden wir es vorerst nicht im Detail erklären. Wir wissen nur, dass es sich um den Teil handelt, der speziell die Synchronize-Funktion behandelt Code.
Nachdem Sie das WakeMainThread-Ereignis ausgeführt haben, verlassen Sie den kritischen Abschnitt und rufen Sie dann WaitForSingleObject auf, um mit dem Warten auf das erstellte Ereignis zu beginnen, bevor Sie den kritischen Abschnitt betreten. Die Funktion dieses Ereignisses besteht darin, auf das Ende der Ausführung dieser Synchronisierungsmethode zu warten. Dies wird später bei der Analyse von CheckSynchronize erläutert.
Beachten Sie, dass Sie nach WaitForSingleObject erneut in den kritischen Abschnitt gelangen, ihn jedoch verlassen, ohne etwas zu tun. Es scheint bedeutungslos, ist aber notwendig!
Denn Enter und Leave im kritischen Abschnitt müssen strikt eins zu eins übereinstimmen. Kann es also wie folgt geändert werden:
wenn Assigned(WakeMainThread), dann
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
Endlich
LeaveCriticalSection(ThreadLock);
Ende;
Der größte Unterschied zwischen dem obigen Code und dem Originalcode besteht darin, dass WaitForSingleObject auch in den Einschränkungen des kritischen Abschnitts enthalten ist. Es scheint keine Auswirkungen zu haben und den Code erheblich zu vereinfachen, aber ist das wirklich möglich?
Tatsächlich nein!
Denn wir wissen, dass andere Threads nach dem kritischen Abschnitt angehalten werden, wenn sie erneut eintreten möchten. Die WaitFor-Methode unterbricht den aktuellen Thread und wird erst aktiviert, wenn sie auf SetEvent anderer Threads wartet. Wenn der Code wie oben beschrieben geändert wird und der SetEvent-Thread auch den kritischen Abschnitt betreten muss, tritt ein Deadlock auf (die Theorie des Deadlocks finden Sie in den Informationen zu den Betriebssystemprinzipien).
Deadlock ist einer der wichtigsten Aspekte der Thread-Synchronisation!
Abschließend wird das zu Beginn erstellte Ereignis freigegeben. Wenn die synchronisierte Methode eine Ausnahme zurückgibt, wird die Ausnahme hier erneut ausgelöst.
(fortgesetzt werden)