Thread class in Delphi
Raptor[Mental Studio]
http://mental.mentsu.com
Part 2
The first is the constructor:
constructor TThread.Create(CreateSuspended: Boolean);
begin
inherited Create;
AddThread;
FSuspended := CreateSuspended;
FCreateSuspended := CreateSuspended;
FHandle := BeginThread(nil, 0, @ThreadPRoc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
if FHandle = 0 then
raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]);
end;
Although this constructor does not have much code, it can be regarded as the most important member because the thread is created here.
After calling TObject.Create through Inherited, the first sentence is to call a process: AddThread, and its source code is as follows:
procedure AddThread;
begin
InterlockedIncrement(ThreadCount);
end;
There is also a corresponding RemoveThread:
procedure RemoveThread;
begin
InterlockedDecrement(ThreadCount);
end;
Their function is very simple, which is to count the number of threads in the process by increasing or decreasing a global variable. It's just that the commonly used Inc/Dec process is not used to increase or decrease variables here, but the pair of InterlockedIncrement/InterlockedDecrement processes are used. They implement exactly the same function, both adding or subtracting one to the variable. But they have one biggest difference, that is, InterlockedIncrement/InterlockedDecrement is thread-safe. That is, they can guarantee correct execution results under multi-threading, but Inc/Dec cannot. Or in operating system theory terms, this is a pair of "primitive" operations.
Take plus one as an example to illustrate the difference in implementation details between the two:
Generally speaking, the operation of adding one to memory data has three steps after decomposition:
1. Read data from memory
2. Data plus one
3. Store in memory
Now assume a situation that may occur when using Inc to perform an increment operation in a two-thread application:
1. Thread A reads data from memory (assumed to be 3)
2. Thread B reads data from memory (also 3)
3. Thread A adds one to the data (now it is 4)
4. Thread B adds one to the data (now it is also 4)
5. Thread A stores the data into the memory (the data in the memory now is 4)
6. Thread B also stores the data into the memory (the data in the memory is still 4, but both threads have added one to it, which should be 5, so there is an incorrect result here)
There is no such problem with the InterlockIncrement process, because the so-called "primitive" is an uninterruptible operation, that is, the operating system can guarantee that thread switching will not occur before a "primitive" is executed. So in the above example, only after thread A finishes executing and storing the data in memory, thread B can start to fetch the number and add one. This ensures that even in a multi-threaded situation, the result will be the same. is correct.
The previous example also illustrates a "thread access conflict" situation, which is why threads need to be "synchronized" (Synchronize). This will be discussed in detail later when synchronization is mentioned.
Speaking of synchronization, there is a digression: Li Ming, a professor at the University of Waterloo in Canada, once raised objections to the word Synchronize being translated as "synchronization" in "thread synchronization". I personally think what he said is actually very reasonable. "Synchronization" in Chinese means "happening at the same time", and the purpose of "thread synchronization" is to avoid this "happening at the same time". In English, Synchronize has two meanings: one is synchronization in the traditional sense (To occur at the same time), and the other is "To Operate in unison" (To Operate in unison). The word Synchronize in "thread synchronization" should refer to the latter meaning, that is, "to ensure that multiple threads maintain coordination and avoid errors when accessing the same data." However, there are still many inaccurately translated words like this in the IT industry. Since it has become a convention, this article will continue to use it. I just explain it here, because software development is a meticulous work, and what should be clarified must never be Can't be vague.
Let’s go back to the constructor of TThread. The most important thing next is this sentence:
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
The Delphi RTL function BeginThread mentioned earlier is used here. It has many parameters, the key ones are the third and fourth parameters. The third parameter is the thread function mentioned earlier, that is, the part of the code executed in the thread. The fourth parameter is the parameter passed to the thread function, here it is the created thread object (ie Self). Among the other parameters, the fifth is used to set the thread to suspend after creation and not execute immediately (the work of starting the thread is determined according to the CreateSuspended flag in AfterConstruction), and the sixth is to return the thread ID.
Now let’s look at the core of TThread: thread function ThreadProc. What's interesting is that the core of this thread class is not a member of the thread, but a global function (because the parameter convention of the BeginThread process can only use global functions). Here is its code:
function ThreadProc(Thread: TThread): Integer;
var
FreeThread: Boolean;
begin
try
if not Thread.Terminated then
try
Thread.Execute;
except
Thread.FFatalException := AcquireExceptionObject;
end;
finally
FreeThread := Thread.FFreeOnTerminate;
Result := Thread.FReturnValue;
Thread.DoTerminate;
Thread.FFinished := True;
SignalSyncEvent;
if FreeThread then Thread.Free;
EndThread(Result);
end;
end;
Although there is not much code, it is the most important part of the entire TThread, because this code is the code that is actually executed in the thread. The following is a line-by-line description of the code:
First, determine the Terminated flag of the thread class. If it is not marked as terminated, call the Execute method of the thread class to execute the thread code. Because TThread is an abstract class and the Execute method is an abstract method, it essentially executes the Execute code in the derived class.
Therefore, Execute is the thread function in the thread class. All code in Execute needs to be considered as thread code, such as preventing access conflicts.
If an exception occurs in Execute, the exception object is obtained through AcquireExceptionObject and stored in the FFatalException member of the thread class.
Finally, there are some finishing touches before the thread ends. The local variable FreeThread records the setting of the FreeOnTerminated attribute of the thread class, and then sets the thread return value to the value of the return value attribute of the thread class. Then execute the DoTerminate method of the thread class.
The code for the DoTerminate method is as follows:
procedure TThread.DoTerminate;
begin
if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
end;
It's very simple, just call the CallOnTerminate method through Synchronize, and the code of the CallOnTerminate method is as follows, which is to simply call the OnTerminate event:
procedure TThread.CallOnTerminate;
begin
if Assigned(FOnTerminate) then FOnTerminate(Self);
end;
Because the OnTerminate event is executed in Synchronize, it is essentially not thread code, but main thread code (see the analysis of Synchronize later for details).
After executing OnTerminate, set the FFinished flag of the thread class to True.
Next, the SignalSyncEvent process is executed, and its code is as follows:
procedure SignalSyncEvent;
begin
SetEvent(SyncEvent);
end;
It is also very simple, just set a global Event: SyncEvent. This article will describe the use of Event in detail later, and the purpose of SyncEvent will be explained in the WaitFor process.
Then it is decided whether to release the thread class based on the FreeOnTerminate setting saved in FreeThread. When the thread class is released, there are some operations. See the following destructor implementation for details.
Finally, EndThread is called to end the thread and the thread return value is returned.
At this point, the thread is completely over.
(to be continued)