Delphi中的線程類(2)

 

首先就是構造函數:

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 := True;

    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結束線程,返回線程返回值。

至此,線程完全結束。

 (待續)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章