Delphi中線(xiàn)程類(lèi)TThread實(shí)現多線(xiàn)程編程1---構造、析構……
參考:http://www.cnblogs.com/rogee/archive/2010/09/20/1832053.html
Delphi中有一個(gè)線(xiàn)程類(lèi)TThread是用來(lái)實(shí)現多線(xiàn)程編程的,這個(gè)絕大多數的Delphi書(shū)籍都有講到,但是基本上都是對TThread類(lèi)的幾個(gè)成員作一簡(jiǎn)單介紹,再說(shuō)明一個(gè) Execute的實(shí)現和 Synchronize 的用法就完了。然而這并不是多線(xiàn)程編程的全部。
線(xiàn)程本質(zhì)上是進(jìn)程中一段并發(fā)運行的代碼。一個(gè)進(jìn)程至少有一個(gè)線(xiàn)程,即所謂的主線(xiàn)程。同時(shí)還可以有多個(gè)子線(xiàn)程。當一個(gè)進(jìn)程中用到超過(guò)一個(gè)線(xiàn)程時(shí),就是所謂的“多線(xiàn)程”
那么這個(gè)所謂的“一段代碼”是如何定義的呢?其實(shí)就是一個(gè)函數或過(guò)程(對Delphi而言)
如果用Windows API來(lái)創(chuàng )建線(xiàn)程的話(huà),是通過(guò)一個(gè)叫做 CreateThread的API函數來(lái)實(shí)現的,它的定義是
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, //線(xiàn)程屬性(用于在NT下進(jìn)行線(xiàn)程的安全屬性設置,在9X下無(wú)效) DWORD dwStackSize, //堆棧大小 LPTHREAD_START_ROUTINE lpStartAddress, //起始地址 LPVOLD lpParameter, //參數 DWORD dwCreationFlags, //創(chuàng )建標志(用于設置線(xiàn)程創(chuàng )建時(shí)候的狀態(tài)) LPDWORD lpThreadId //線(xiàn)程ID );
最后返回線(xiàn)程 Handle。其中的起始地址就是線(xiàn)程函數的入口,直至線(xiàn)程函數結束,線(xiàn)程也就結束了
因為CreateThread的參數很多,而且是Windows API,所以在 C Runtime Library里面提供了一個(gè)通用的線(xiàn)程函數(理論上是可以在任何支持線(xiàn)程的OS中使用):
unsigned long_beginthread(void(_USERENTRY *__start)(void*), unsigned __stksize, void * __arg);
Delphi也提供了一個(gè)相同功能的類(lèi)似函數
function BeginThread(SecurityAttributes: Pointer; StackSize: LongWord; ThreadFunc: TThreadFunc; Parameter: Pointer; CreationFlags: LongWord; var ThreadId: LongWord): Integer;
這三個(gè)函數的功能是基本相同的,它們都是將線(xiàn)程函數中大代碼放到一個(gè)獨立的線(xiàn)程中執行。線(xiàn)程函數與一般函數最大的不同在于,線(xiàn)程函數一啟動(dòng),這三個(gè)線(xiàn)程啟動(dòng)函數就返回了,主線(xiàn)程繼續向下執行,而線(xiàn)程函數在一個(gè)獨立的線(xiàn)程中執行,它要執行多久,什么時(shí)候返回,主線(xiàn)程是不管也不知道的。
正常情況下,線(xiàn)程函數返回之后,線(xiàn)程就終止了。但是也有其他方式:
Windows API:
VOID ExitThread(DWORD dwExitCode);
C Runtime Library:
void _endthread(void);
Delphi Runtime Library:
procedure EndThread(ExitCode: Integer);
為了記錄一些必要的線(xiàn)程數據(狀態(tài)/屬性等),OS會(huì )為線(xiàn)程創(chuàng )建一個(gè)內部Object,如在Windows中那個(gè)Handle 便是這個(gè)內部Object的 Handle,所以在線(xiàn)程結束的時(shí)候還應該釋放這個(gè)Object
雖然說(shuō)用API 和 RTL(Runtime Library)已經(jīng)可以很方便地進(jìn)行多線(xiàn)程編程了,但是還是需要進(jìn)行較多的細節處理,為此 Delphi在Classes單元中對線(xiàn)程做了一個(gè)較好的封裝,這就是VCL的線(xiàn)程類(lèi): TThread
使用這個(gè)類(lèi)也很簡(jiǎn)單,大多數的Delphi都有說(shuō),基本用法是:
1.先從TThread派生一個(gè)自己的線(xiàn)程類(lèi)(因為T(mén)Thread是一個(gè)抽象類(lèi),不能生成實(shí)例)
2.然后是Override抽象方法:Execute(這就是線(xiàn)程函數,也就是在線(xiàn)程中執行的代碼部分)
3.如果需要用到可視VCL對象,還需要通過(guò)Synchronize過(guò)程進(jìn)行
本文接下來(lái)要討論的是TThread類(lèi)是如何對線(xiàn)程進(jìn)行封裝的,也就是深入探究一下TThread類(lèi)的實(shí)現。因為只有真正地了解它,才能更好的使用它
下面是Delphi 7 中TThread類(lèi)的聲明(本文只討論 Windows平臺下的實(shí)現,所以去掉了所有有關(guān)Linux平臺部分的代碼)
TThread = class private FHandle: THandle; FThreadID: THandle; FCreateSuspended: Boolean; FTerminated: Boolean; FSuspended: Boolean; FFreeOnTerminate: Boolean; FFinished: Boolean; FReturnValue: Integer; FOnTerminate: TNotifyEvent; FSynchronize: TSynchronizeRecord; FFatalException: TObject; procedure CallOnTerminate; class procedure Synchronize(ASyncRec: PSynchronizeRecord); overload; function GetPriority: TThreadPriority; procedure SetPriority(Value: TThreadPriority); procedure SetSuspended(Value: Boolean); protected procedure CheckThreadError(ErrCode: Integer); overload; procedure CheckThreadError(Success: Boolean); overload; procedure DoTerminate; virtual; procedure Execute; virtual; abstract; procedure Synchronize(Method: TThreadMethod); overload; property ReturnValue: Integer read FReturnValue write FReturnValue; property Terminated: Boolean read FTerminated; public constructor Create(CreateSuspended: Boolean); destructor Destroy; override; procedure AfterConstruction; override; procedure Resume; procedure Suspend; procedure Terminate; function WaitFor: LongWord; class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload; class procedure StaticSynchronize(AThread: TThread; AMethod: TThreadMethod); property FatalException: TObject read FFatalException; property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate; property Handle: THandle read FHandle; property Priority: TThreadPriority read GetPriority write SetPriority; property Suspended: Boolean read FSuspended write SetSuspended; property ThreadID: THandle read FThreadID; property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate; end;
TThread類(lèi)在Delphi的RTL里面算是比較簡(jiǎn)單的類(lèi),類(lèi)成員也不多,類(lèi)屬性都很簡(jiǎn)單明白,本文將只對幾個(gè)比較重要的類(lèi)成員方法和唯一的事件:OnTerminate做詳細分析
首先是構造函數
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.CreateRssFmt(@SThreadCreateError,[SysErrorMessage(GetLastError)]); end;
雖然這個(gè)構造函數沒(méi)有多少代碼,但是卻可以算是最重要的一個(gè)成員,因為線(xiàn)程就是在這里被創(chuàng )建的。
在通過(guò)Inherited 調用TObject.Create之后,第一句就是調用一個(gè)過(guò)程:AddThread,其源碼如下
procedure AddThread; begin InterlockedIncrement(ThreadCount); end;
同樣有對應的RemoveThread
procedure RemoveThread; begin InterlockedDecrement(ThreadCount); end;
它們的功能很簡(jiǎn)單,就是通過(guò)增減一個(gè)全局變量來(lái)統計進(jìn)程中的線(xiàn)程數。只是這里用于增減變量的并不是常用的Inc/Dec過(guò)程,而是用InterlockedIncrement/InterlockedDecrement這對過(guò)程。,它們實(shí)現的功能完全一樣,都是對變量加一或減一。但是它們有一個(gè)最大的區別,那就是InterlockedIncrement/InterlockedDecrement是線(xiàn)程安全的。即它們在多線(xiàn)程下能保證執行結果的正確,而Inc/Dec不能。或者按操作系統理論中的術(shù)語(yǔ)來(lái)說(shuō),這是一對“原語(yǔ)”操作。以加一為例來(lái)說(shuō)明二者實(shí)現細節上的不同:
一般而言,對內存數據加一的操作分解之后有三步:
1)從內存讀出數據
2)數據加一
3)存入內存
現在假設在一個(gè)有兩個(gè)線(xiàn)程的進(jìn)程中用Inc進(jìn)行加一操作,可能出現下面這種情況:
1)線(xiàn)程A從內存中讀出數據(假設為3)
2)線(xiàn)程B也從內存中讀出數據(也是3)
3)線(xiàn)程A對數據加一(現在是4)
4)線(xiàn)程B對數據加一(現在也是4)
5)線(xiàn)程A將數據存入內存(現在內存中的數據是4)
6)線(xiàn)程B也將數據存入內存(現在的內存中的數據還是4,但是兩個(gè)線(xiàn)程都對它加了一,應該是5才對,所以這里出現了錯誤的結果
而用InterlockedIncrement過(guò)程則沒(méi)有這個(gè)問(wèn)題,因為所謂“原語(yǔ)”是一種不可中斷的操作,即操作系統能保證在一個(gè)“原語(yǔ)”執行完畢之前不會(huì )進(jìn)行線(xiàn)程切換。所以在上面的例子中,只有當線(xiàn)程A執行完并將數據存入到內存之后,線(xiàn)程B才可以開(kāi)始從中取數并進(jìn)行加一操作,這就保證了即使是在多線(xiàn)程情況下,結果一定會(huì )使正確的。
前面的那個(gè)例子也說(shuō)明一種“線(xiàn)程訪(fǎng)問(wèn)沖突”的情況,這也是為什么線(xiàn)程之間需要“同步”(Synchronize),關(guān)于這個(gè),在后面說(shuō)到同步的時(shí)候還會(huì )再詳細討論
說(shuō)到同步,有一個(gè)題外話(huà):加拿大滑鐵盧大學(xué)的教授李明曾就Synchronize一詞在“線(xiàn)程同步”中被譯作“同步”提出過(guò)異議,個(gè)人認為他說(shuō)的其實(shí)很有道理。在中文中“同步”的意思是“同時(shí)發(fā)生”,而“線(xiàn)程同步”目的就是避免這種“同時(shí)發(fā)生”的事情。而在英文中,Synchronize的意思有兩個(gè):一個(gè)是傳統意義上的同步(To occur at the same time),另一個(gè)是“協(xié)調一致”(To operate in unison)。在“線(xiàn)程同步”中的Synchronize一詞應該是指后面一種意思,即“保證多個(gè)線(xiàn)程在訪(fǎng)問(wèn)同一數據時(shí),保持協(xié)調一致,避免出錯”。不過(guò)像這樣譯得不準的詞在IT業(yè)還有很多,既然已經(jīng)是約定俗成了,本文也將繼續沿用,只是在這里說(shuō)明一下,因為軟件開(kāi)發(fā)是一項細致的工作,該弄清楚的,絕不能含糊。
扯遠了,回到TThread的構造函數上,接下來(lái)最重要的就是這句了
FHandle:= BeginThread(nil, 0, @ThreadProc(Self), Create_SUSPENDED, FThreadID);
這里就用到了前面所說(shuō)的Delphi RTL函數BeginThread,它有很多參數,關(guān)鍵是第三、四兩個(gè)參數。第三個(gè)參數就是前面說(shuō)到的線(xiàn)程函數,即在線(xiàn)程中執行的代碼部分。第四個(gè)參數則是傳遞給線(xiàn)程函數的參數,這里就是創(chuàng )建的線(xiàn)程對象(即Self)。其他的參數中,第五個(gè)是用于設置線(xiàn)程在創(chuàng )建之后即掛起,不立即執行(啟動(dòng)線(xiàn)程的工作實(shí)在 AfterConstruction中根據 CtreateSuspended標識來(lái)決定的),第六個(gè)是返回線(xiàn)程ID
現在來(lái)看 TThread的核心:線(xiàn)程函數ThreadProc。有意思的是這個(gè)線(xiàn)程類(lèi)的核心卻不是線(xiàn)程的成員,而是一個(gè)全局函數(因為BeginThread過(guò)程的參數約定只能用全局函數)。下面是它的代碼
fucntion 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;
雖然這里也沒(méi)有多少代碼,但卻是整個(gè)TThread中最重要的部分,因為這段代碼是真正在線(xiàn)程中執行的代碼。下面對代碼做逐行說(shuō)明:
首先判斷線(xiàn)程類(lèi)的Terminated 標識,如果未被標志位終止,則調用線(xiàn)程類(lèi)的Execute方法執行線(xiàn)程代碼,因為T(mén)Thread是抽象類(lèi),Execute方法是抽象方法,所以本質(zhì)上是執行派生類(lèi)中的Execute代碼。
所以說(shuō),Execute 就是線(xiàn)程類(lèi)中的線(xiàn)程函數,所有在Execute 中的代碼都需要被當做線(xiàn)程代碼來(lái)考慮。如防止訪(fǎng)問(wèn)沖突等。如果Execute發(fā)生異常,則通過(guò)AcquireExceptionObject取得異常對象,并存入線(xiàn)程類(lèi)的FFatalException成員中
最后是線(xiàn)程結束之前做的一些收尾工作。局部變量FreeThread記錄了線(xiàn)程類(lèi)的FreeOnTerminate屬性的設置,然后將線(xiàn)程返回值設置為線(xiàn)程類(lèi)的返回值屬性的值。然后執行線(xiàn)程類(lèi)的 DoTerminate方法
DoTerminate方法的代碼如下
procedure TThread.DoTerminate; begin if Assigned(FOnTerminate) then Synchronize(CallOnTerminate); end;
很簡(jiǎn)單,就是通過(guò)Synchronize 來(lái)調用 CallOnTerminate 方法,而CallOnTerminate 方法的代碼如下,就是簡(jiǎn)單地調用OnTerminate 事件:
procedure TThread.CallOnTerminate; begin if Assigned(FOnTerminate) then FOnTerminate(Self); end;
因為 OnTerminate事件是在 Synchronize 中執行的,所以本質(zhì)上它并不是線(xiàn)程代碼,而是主線(xiàn)程代碼(具體見(jiàn)后面對Synchronize的分析)
執行完OnTerminate之后,將線(xiàn)程類(lèi)的 FFinished標志設置為T(mén)rue。接下來(lái)執行SignalSyncEvent過(guò)程,其代碼如下
procedure SignalSyncEvent begin SetEvent(SyncEvent); end;
也很簡(jiǎn)單,就是設置一下一個(gè)全局變量Event:SyncEvent,關(guān)于Event 的使用,本文將在后文詳述,而SyncEvent 的用途將在 WaitFor過(guò)程中說(shuō)明
然后根據 FreeThread中保存的FreeOnTerminate設置決定是否釋放線(xiàn)程類(lèi),在線(xiàn)程類(lèi)釋放時(shí),還有一些操作,詳見(jiàn)下面的析構函數實(shí)現。
最后調用 EndThread 結束線(xiàn)程,返回線(xiàn)程返回值。至此,線(xiàn)程完全結束。
說(shuō)完構造函數,再看析構函數
destructor TThread.Destory; begin if (FThreadID <> 0) and not FFinished then begin Terminate; if FCreateSuspended then Resume; WaitFor; end; if FHandle <> 0 then CloseHandle(FHandle); inherited Destory; FFatalException.Free; RemoveThread; end;
在線(xiàn)程對象被釋放之前,首先要檢查線(xiàn)程是否還在執行中,如果線(xiàn)程還在執行中(線(xiàn)程ID不為0,并且線(xiàn)程結束標志未設置),則調用Terminate 過(guò)程結束線(xiàn)程。Terminate 過(guò)程知識簡(jiǎn)單地設置線(xiàn)程類(lèi)的 Terminated標志,如下面的代碼:
procedure TThread.Terminate; begin FTerminated:= True; end;
所以線(xiàn)程仍然必須繼續執行到正常結束之后才行,而不是立即終止線(xiàn)程,這一點(diǎn)要注意
在這里說(shuō)一些題外話(huà):很多人問(wèn)過(guò)我,如何才能“立即”終止線(xiàn)程(當前是指用TThread 創(chuàng )建的線(xiàn)程)。結果當然是不行!終止線(xiàn)程的唯一的方法就是讓Execute 方法執行完畢,所以一般來(lái)說(shuō),要讓你的線(xiàn)程能盡快終止,必須在Execute 方法中在較短的時(shí)間內不斷檢查T(mén)erminated標志,以便能及時(shí)地退出。這是設計線(xiàn)程代碼的一個(gè)很重要的原則!
當然如果你一定要能“立即”退出線(xiàn)程,那么TThread 類(lèi)不是一個(gè)好的選擇,因為如果用API強制終止線(xiàn)程的話(huà),最終會(huì )導致TThread 線(xiàn)程對象不能被正確釋放,在對象析構時(shí)出現 Access Violation。這種情況你只能使用API或者RTL函數來(lái)創(chuàng )建線(xiàn)程
如果線(xiàn)程結束處于啟動(dòng)掛起狀態(tài),則線(xiàn)程轉入到運行狀態(tài),然后調用WaitFor進(jìn)行等待,其功能就是等待到線(xiàn)程結束后才繼續向下執行。關(guān)于WaitFor的實(shí)現,將放到后面說(shuō)明。
線(xiàn)程結束后,關(guān)閉線(xiàn)程Handle(正常線(xiàn)程創(chuàng )建的情況下Handle都是存在的),釋放操作系統創(chuàng )建的線(xiàn)程對象。
然后調用TObject.Destory 釋放本對象,并釋放已經(jīng)捕獲的異常對象,最后調用RemoveThread減少進(jìn)程的線(xiàn)程數
其他關(guān)于 Suspend/Resume及線(xiàn)程優(yōu)先級設置等方面,不是本文的重點(diǎn),不再贅述。下面討論的是本文的另兩個(gè)重點(diǎn):Synchronize和WaitFor。具體的見(jiàn)下一篇博客