Delphi中的執行緒類
猛禽[Mental Studio]
http://mental.mentsu.com
之二
首先就是建構子:
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;
雖然這個建構函式沒有太多程式碼,但卻可以算是最重要的一個成員,因為執行緒就是在這裡被創建的。
透過Inherited呼叫TObject.Create後,第一句就是呼叫一個程序:AddThread,其原始碼如下:
procedure AddThread;
begin
InterlockedIncrement(ThreadCount);
end;
同樣有對應的RemoveThread:
procedure RemoveThread;
begin
InterlockedDecrement(ThreadCount);
end;
它們的功能很簡單,就是透過增減一個全域變數來統計行程中的執行緒數。只是這裡用來增減變數的並不是常用的Inc/Dec過程,而是用了InterlockedIncrement/InterlockedDecrement這一對過程,它們實現的功能完全一樣,都是對變數加一或減一。但它們有一個最大的差別,那就是InterlockedIncrement/InterlockedDecrement是執行緒安全的。即它們在多執行緒下能保證執行結果正確,而Inc/Dec不能。或者以作業系統理論中的術語來說,這是一對「原語」操作。
以加一為例來說明二者實作細節上的不同:
一般來說,記憶體資料加一的操作分解以後有三個步驟:
1、 從記憶體讀出數據
2、 數據加一
3、 存入內存
現在假設在一個兩個執行緒的應用中用Inc進行加一操作可能出現的一種情況:
1、 線程A從記憶體中讀出資料(假設為3)
2、 線程B從記憶體中讀出資料(也是3)
3、 線程A對資料加一(現在是4)
4、 線程B對資料加一(現在也是4)
5. 線程A將資料存入記憶體(現在記憶體中的資料是4)
6. 線程B也將資料存入記憶體(現在記憶體中的資料還是4,但兩個執行緒都對它加了一,應該是5才對,所以這裡出現了錯誤的結果)
而用InterlockIncrement過程則沒有這個問題,因為所謂「原語」是一種不可中斷的操作,就是作業系統能保證在一個「原語」執行完畢前不會進行執行緒切換。所以在上面那個例子中,只有當線程A執行完將資料存入記憶體後,線程B才可以開始從中取數並進行加一操作,這樣就保證了即使是在多線程情況下,結果也一定會是正確的。
前面那個例子也說明一種「執行緒存取衝突」的情況,這也就是為什麼執行緒之間需要「同步」(Synchronize),關於這個,在後面說到同步時還會再詳細討論。
說到同步,有一個題外話:加拿大滑鐵盧大學的教授李明曾就Synchronize一詞在“線程同步”中被譯作“同步”提出過異議,個人認為他說的其實很有道理。在中文中“同步”的意思是“同時發生”,而“線程同步”目的就是避免這種“同時發生”的事情。而在英文中,Synchronize的意思有兩個:一個是傳統意義上的同步(To occur at the same time),另一個是「協調一致」(To Operate in unison)。在「執行緒同步」中的Synchronize一詞應該是指後面一種意思,即「保證多個執行緒在存取相同資料時,保持協調一致,避免出錯」。不過像這樣譯得不準的詞在IT業還有很多,既然已經是約定俗成了,本文也將繼續沿用,只是在這裡說明一下,因為軟體開發是一項細緻的工作,該弄清楚的,絕不能含糊。
扯遠了,回到TThread的建構子上,接下來最重要就是這句話了:
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
這裡就用到了前面說到的Delphi RTL函數BeginThread,它有很多參數,關鍵的是第三、四兩個參數。第三個參數就是前面說到的線程函數,也就是在線程中執行的程式碼部分。第四個參數則是傳遞給執行緒函數的參數,這裡就是建立的執行緒物件(即Self)。其它的參數中,第五個是用來設定線程在創建後即掛起,不立即執行(啟動線程的工作是在AfterConstruction中根據CreateSuspended標誌來決定的),第六個是返回線程ID。
現在來看TThread的核心:線程函數ThreadProc。有趣的是這個線程類別的核心卻不是線程的成員,而是一個全域函數(因為BeginThread過程的參數約定只能用全域函數)。下面是它的程式碼:
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 := 真;
SignalSyncEvent;
if FreeThread then Thread.Free;
EndThread(Result);
end;
end;
雖然也沒有多少程式碼,但卻是整個TThread中最重要的部分,因為這段程式碼是真正在執行緒中執行的程式碼。以下對程式碼作逐行說明:
首先判斷線程類別的Terminated標誌,如果未被標誌為終止,則呼叫線程類別的Execute方法執行線程程式碼,因為TThread是抽象類,Execute方法是抽象方法,所以本質上是執行衍生類別中的Execute程式碼。
所以說,Execute就是線程類別中的線程函數,所有在Execute中的程式碼都需要當作線程程式碼來考慮,例如防止存取衝突等。
如果Execute發生異常,則透過AcquireExceptionObject取得異常對象,並存入線程類別的FFatalException成員中。
最後是線程結束前做的一些收尾工作。局部變數FreeThread記錄了線程類別的FreeOnTerminated屬性的設置,然後將線程返回值設置為線程類別的返回值屬性的值。然後執行線程類別的DoTerminate方法。
DoTerminate方法的程式碼如下:
procedure TThread.DoTerminate;
begin
if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
end;
很簡單,就是透過Synchronize來呼叫CallOnTerminate方法,而CallOnTerminate方法的程式碼如下,就是簡單地呼叫OnTerminate事件:
procedure TThread.CallOnTerminate;
begin
if Assigned(FOnTerminate) then FOnTerminate(Self);
end;
因為OnTerminate事件是在Synchronize中執行的,所以本質上它並不是線程程式碼,而是主執行緒程式碼(具體見後面對Synchronize的分析)。
執行完OnTerminate後,將線程類別的FFinished標誌設為True。
接下來執行SignalSyncEvent過程,其程式碼如下:
procedure SignalSyncEvent;
begin
SetEvent(SyncEvent);
end;
也很簡單,就是設定一個全域Event:SyncEvent,關於Event的使用,本文將在後文詳述,而SyncEvent的用途將在WaitFor過程中說明。
然後根據FreeThread中儲存的FreeOnTerminate設定決定是否釋放線程類,在線程類釋放時,還有一些些操作,詳見接下來的析構函數實作。
最後呼叫EndThread結束線程,回傳線程回傳值。
至此,線程完全結束。
(待續)