Thread-Klasse in Delphi
Raptor[Mental Studio]
http://mental.mentsu.com
Teil 2
Der erste ist der Konstruktor:
Konstruktor TThread.Create(CreateSuspended: Boolean);
beginnen
geerbtes Erstellen;
AddThread;
FSuspended := CreateSuspended;
FCreateSuspended := CreateSuspended;
FHandle := BeginThread(nil, 0, @ThreadPRoc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
wenn FHandle = 0 dann
raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]);
Ende;
Obwohl dieser Konstruktor nicht viel Code hat, kann er als das wichtigste Mitglied angesehen werden, da der Thread hier erstellt wird.
Nach dem Aufruf von TObject.Create über Inherited besteht der erste Satz darin, einen Prozess aufzurufen: AddThread, und sein Quellcode lautet wie folgt:
Prozedur AddThread;
beginnen
InterlockedIncrement(ThreadCount);
Ende;
Es gibt auch einen entsprechenden RemoveThread:
Prozedur RemoveThread;
beginnen
InterlockedDecrement(ThreadCount);
Ende;
Ihre Funktion ist sehr einfach: Sie zählen die Anzahl der Threads im Prozess durch Erhöhen oder Verringern einer globalen Variablen. Es ist nur so, dass hier nicht der häufig verwendete Inc/Dec-Prozess zum Erhöhen oder Verringern von Variablen verwendet wird, sondern das Paar der InterlockedIncrement/InterlockedDecrement-Prozesse. Sie implementieren genau die gleiche Funktion, indem sie jeweils eins zur Variablen addieren oder subtrahieren. Sie haben jedoch einen größten Unterschied: InterlockedIncrement/InterlockedDecrement ist threadsicher. Das heißt, sie können korrekte Ausführungsergebnisse unter Multithreading garantieren, Inc/Dec jedoch nicht. Oder um es in der Betriebssystemtheorie auszudrücken: Dies ist ein Paar „primitiver“ Operationen.
Nehmen Sie Plus Eins als Beispiel, um den Unterschied in den Implementierungsdetails zwischen den beiden zu veranschaulichen:
Im Allgemeinen besteht der Vorgang des Hinzufügens von eins zu Speicherdaten nach der Zerlegung aus drei Schritten:
1. Daten aus dem Speicher lesen
2. Daten plus eins
3. Im Speicher speichern
Nehmen wir nun eine Situation an, die auftreten kann, wenn Inc zum Ausführen einer Inkrementierungsoperation in einer Zwei-Thread-Anwendung verwendet wird:
1. Thread A liest Daten aus dem Speicher (angenommen 3)
2. Thread B liest Daten aus dem Speicher (auch 3)
3. Thread A fügt eins zu den Daten hinzu (jetzt sind es 4)
4. Thread B fügt eins zu den Daten hinzu (jetzt sind es auch 4)
5. Thread A speichert die Daten im Speicher (die Daten im Speicher sind jetzt 4)
6. Thread B speichert die Daten auch im Speicher (die Daten im Speicher sind immer noch 4, aber beide Threads haben einen hinzugefügt, der 5 sein sollte, daher gibt es hier ein falsches Ergebnis)
Beim InterlockIncrement-Prozess gibt es kein solches Problem, da es sich bei dem sogenannten „Primitiv“ um einen unterbrechungsfreien Vorgang handelt, d. h. das Betriebssystem kann garantieren, dass kein Threadwechsel erfolgt, bevor ein „Primitiv“ ausgeführt wird. Im obigen Beispiel kann Thread B also erst dann beginnen, die Zahl abzurufen und hinzuzufügen, wenn Thread A die Ausführung und Speicherung der Daten im Speicher abgeschlossen hat. Dadurch wird sichergestellt, dass das Ergebnis auch in einer Multithread-Situation dasselbe ist richtig.
Das vorherige Beispiel veranschaulicht auch eine Situation mit einem „Thread-Zugriffskonflikt“, weshalb Threads „synchronisiert“ werden müssen (Synchronize). Dies wird später ausführlich besprochen, wenn von der Synchronisierung die Rede ist.
Apropos Synchronisation, es gibt einen Exkurs: Li Ming, Professor an der University of Waterloo in Kanada, hat einmal Einwände dagegen erhoben, dass das Wort Synchronize mit „Synchronisation“ in „Thread-Synchronisation“ übersetzt wird. Ich persönlich denke, dass das, was er sagte, tatsächlich stimmt sehr vernünftig. „Synchronisation“ bedeutet auf Chinesisch „zur gleichen Zeit geschehen“, und der Zweck der „Thread-Synchronisation“ besteht darin, zu verhindern, dass dies „zur gleichen Zeit geschieht“. Im Englischen hat „Synchronisieren“ zwei Bedeutungen: Die eine ist Synchronisation im herkömmlichen Sinne (gleichzeitig stattfinden) und die andere ist „Gleichzeitig arbeiten“ (Gleichzeitig arbeiten). Das Wort „Synchronisieren“ in „Thread-Synchronisation“ sollte sich auf die letztere Bedeutung beziehen, nämlich „sicherzustellen, dass mehrere Threads die Koordination aufrechterhalten und Fehler beim Zugriff auf dieselben Daten vermeiden“. Allerdings gibt es in der IT-Branche immer noch viele falsch übersetzte Wörter wie dieses. Da es sich um eine Konvention handelt, werde ich es hier nur erläutern, da Softwareentwicklung eine sorgfältige Arbeit ist und was geklärt werden sollte darf niemals sein. Kann nicht vage sein.
Gehen wir noch einen Schritt weiter, zurück zum Konstruktor von TThread, der nächstwichtigste Punkt ist dieser Satz:
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
Hier wird die zuvor erwähnte Delphi-RTL-Funktion BeginThread verwendet. Sie verfügt über viele Parameter, die wichtigsten sind der dritte und vierte Parameter. Der dritte Parameter ist die zuvor erwähnte Thread-Funktion, dh der Teil des Codes, der im Thread ausgeführt wird. Der vierte Parameter ist der an die Thread-Funktion übergebene Parameter, hier ist es das erstellte Thread-Objekt (dh Self). Unter den anderen Parametern wird der fünfte verwendet, um den Thread so einzustellen, dass er nach der Erstellung angehalten und nicht sofort ausgeführt wird (die Arbeit zum Starten des Threads wird anhand des CreateSuspended-Flags in AfterConstruction bestimmt), und der sechste dient dazu, die Thread-ID zurückzugeben.
Schauen wir uns nun den Kern von TThread an: die Thread-Funktion ThreadProc. Interessant ist, dass der Kern dieser Thread-Klasse kein Mitglied des Threads ist, sondern eine globale Funktion (da die Parameterkonvention des BeginThread-Prozesses nur globale Funktionen verwenden kann). Hier ist der Code:
Funktion ThreadProc(Thread: TThread): Integer;
var
FreeThread: Boolean;
beginnen
versuchen
Wenn nicht Thread.Terminated, dann
versuchen
Thread.Execute;
außer
Thread.FFatalException := AcquireExceptionObject;
Ende;
Endlich
FreeThread := Thread.FFreeOnTerminate;
Ergebnis := Thread.FReturnValue;
Thread.DoTerminate;
Thread.FFinished := True;
SignalSyncEvent;
wenn FreeThread dann Thread.Free;
EndThread(Ergebnis);
Ende;
Ende;
Obwohl es nicht viel Code gibt, ist es der wichtigste Teil des gesamten TThread, da dieser Code der Code ist, der tatsächlich im Thread ausgeführt wird. Das Folgende ist eine zeilenweise Beschreibung des Codes:
Bestimmen Sie zunächst das Terminated-Flag der Thread-Klasse. Rufen Sie die Execute-Methode der Thread-Klasse auf, um den Thread-Code auszuführen. Da TThread eine abstrakte Klasse und die Execute-Methode im Wesentlichen eine abstrakte Methode ist führt den Execute-Code in der abgeleiteten Klasse aus.
Daher ist Execute die Thread-Funktion in der Thread-Klasse. Der gesamte Code in Execute muss als Thread-Code betrachtet werden, um beispielsweise Zugriffskonflikte zu verhindern.
Wenn in Execute eine Ausnahme auftritt, wird das Ausnahmeobjekt über AcquireExceptionObject abgerufen und im FFatalException-Mitglied der Thread-Klasse gespeichert.
Zum Abschluss gibt es noch einige Feinarbeiten, bevor der Thread endet. Die lokale Variable FreeThread zeichnet die Einstellung des FreeOnTerminated-Attributs der Thread-Klasse auf und setzt dann den Thread-Rückgabewert auf den Wert des Rückgabewertattributs der Thread-Klasse. Führen Sie dann die DoTerminate-Methode der Thread-Klasse aus.
Der Code für die DoTerminate-Methode lautet wie folgt:
Prozedur TThread.DoTerminate;
beginnen
if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
Ende;
Es ist ganz einfach: Rufen Sie einfach die CallOnTerminate-Methode über Synchronize auf. Der Code der CallOnTerminate-Methode lautet wie folgt: Rufen Sie einfach das OnTerminate-Ereignis auf:
Prozedur TThread.CallOnTerminate;
beginnen
if Assigned(FOnTerminate) then FOnTerminate(Self);
Ende;
Da das OnTerminate-Ereignis in Synchronize ausgeführt wird, handelt es sich nicht unbedingt um Thread-Code, sondern um Haupt-Thread-Code (Einzelheiten finden Sie später in der Analyse von Synchronize).
Setzen Sie nach der Ausführung von OnTerminate das FFinished-Flag der Thread-Klasse auf True.
Als nächstes wird der SignalSyncEvent-Prozess ausgeführt und sein Code lautet wie folgt:
Prozedur SignalSyncEvent;
beginnen
SetEvent(SyncEvent);
Ende;
Es ist auch sehr einfach: Legen Sie einfach ein globales Ereignis fest: SyncEvent. In diesem Artikel wird die Verwendung von Event später ausführlich beschrieben, und der Zweck von SyncEvent wird im WaitFor-Prozess erläutert.
Anschließend wird anhand der in FreeThread gespeicherten FreeOnTerminate-Einstellung entschieden, ob die Thread-Klasse freigegeben werden soll. Weitere Informationen finden Sie in der folgenden Destruktorimplementierung.
Schließlich wird EndThread aufgerufen, um den Thread zu beenden, und der Thread-Rückgabewert wird zurückgegeben.
An dieser Stelle ist der Thread komplett zu Ende.
(fortgesetzt werden)