上次跟大家分享了線程的標准代碼,其實在線程的使用中最重要的是線程的同步問題,如果你在使用線程后,發現你的界面經常被卡死,或者無法顯示出來,顯示混亂,你的使用的變量值老是不按預想的變化,結果往往出乎意料,那么你很有可能是忽略了線程同步的問題。
當有多個線程的時候,經常需要去同步這些線程以訪問同一個數據或資源。例如,假設有一個程序,其中一個線程用於把文件讀到內存,而另一個線程用於統計文件中的字符數。當然,在把整個文件調入內存之前,統計它的計數是沒有意義的。但是,由於每個操作都有自己的
線程,操作系統會把兩個線程當作是互不相干的任務分別執行,這樣就可能在沒有把整個文
件裝入內存時統計字數。為解決此問題,你必須使兩個線程同步工作。存在一些線程同步地
址的問題,Windows 提供了許多線程同步的方式。在本節您將看到使用臨界區、互斥、信
號量、事件、全局原子和Synchronize 函數來解決線程同步的問題。
下面的同步技術一般均有兩種使用方式,一種是直接使用Windows API 函數,一種是使用
由Delphi 對API 函數進行封裝的類。
以下函數以Delphi 2009 中的函數格式為准。
1. Critical Sections 臨界區
臨界區是一種最直接的線程同步方式。所謂臨界區,就是一次只能由一個線程來執行的一段
代碼。例如把初始化數組的代碼放在臨界區內,另一個線程在第一個線程處理完之前是不會
被執行的。臨界區非常適合於序列化對一個進程中的數據的訪問,因為它們的速度很快。
(1). 使用EnterCriticalSection( ) 和LeaveCriticalSection( ) API 函數
在使用臨界區之前, 必須定義一個TRTLCriticalSection 類型的記錄變量並使用
InitializeCriticalSection( ) 過程來初始化臨界區。該過程多半在窗體創建時或在程序初始化時
執行。
其聲明如下:
procedure InitializeCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall;
lpCriticalSection 參數是一個TRTLCriticalSection 類型的記錄, 並且是變參。至於
TRTLCriticalSection 是如何定義的,這並不重要,因為很少需要查看這個記錄中的具體內容。
只需要在lpCriticalSection 中傳遞未初始化的記錄, InitializeCriticalSection( ) 過程就會填
充這個記錄。
注意:Microsoft 故意隱瞞了TRTLCriticalSection 的細節。因為,其內容在不同的硬件平台
上是不同的。在基於Intel 的平台上,TRTLCriticalSection 包含一個計數器、一個指示當前
線程句柄的域和一個系統事件的句柄。在Alpha 平台上,計數器被替換為一種Alpha-CPU數據結構,稱為spinlock 。
在記錄被填充后,我們就可以開始創建臨界區了。這時我們需要用EnterCriticalSection( ) 和
LeaveCriticalSection( ) 來封裝代碼塊,這兩個函數分別代表進入和離開臨界區,將要同步的
代碼塊放在這兩個函數中間。在第一個線程調用了EnterCriticalSection( ) 之后,所有別的
線程就不能再進入代碼塊並掛起等待第一個線程離開臨界區。下一個線程要等第一個線程調
用LeaveCriticalSection( ) 后才能被喚醒。這兩個過程的聲明如下:
procedure EnterCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall; //進入臨界區
procedure LeaveCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall; //離開臨界
區
正如你所想的,參數lpCriticalSection 就是由InitializeCriticalSection( ) 填充的記錄。
如果在某個子線程執行EnterCriticalSection( ) 前,已經有另一個線程進入臨界區且還未離
開臨界區,則該子線程將掛起並無限期等待另一個線程離開臨界區,要想不掛起且0 時間
等待,必須使用TryEnterCriticalSection( ) 。該過程聲明如下:
function TryEnterCriticalSection(var lpCriticalSection: TRTLCriticalSection): BOOL; stdcall;
TryEnterCriticalSection( ) 不同於EnterCriticalSection( ) 的聲明在於多出一個布爾型的返回
值,如果返回True 代表成功進入臨界區,如果返回False 代表臨界區已占用且不進入臨界
區。運用這個函數,線程能夠迅速查看它是否可以訪問某個共享資源,如果不能訪問,那么
它可以繼續執行某些其他操作,而不必進行等待。
使用TryEnterCriticalSection( ) ,必須判斷其返回值。
當你不需要臨界區時,應當調用DeleteCriticalSection( ) 過程刪除臨界區,該函數多半在窗
體銷毀時或程序終止前執行。下面是它的聲明:
procedure DeleteCriticalSection(var lpCriticalSection : TRTLCriticalSection); stdcall;
例:
type TMyThread = class(TThread) protected procedure Execute; override; public constructor Create; virtual; end; var Form1 : TForm1; CriticalSection : TRTLCriticalSection;//定義臨界區 implementation {$R *.dfm} var tick: Integer = 1; procedure TMyThread.Execute; begin EnterCriticalSection(CriticalSection);//進入臨界區 try Form1.Edit1.Text := IntToStr(tick); Inc(tick); Sleep(10); finally LeaveCriticalSection(CriticalSection); //離開臨界區 end; end; constructor TMyThread.Create; begin inherited Create(False); FreeOnTerminate := True; end; procedure TForm1.RzButton1Click(Sender : TObject); var index: Integer; begin for index := 0 to 15 do TMyThread.Create; end; procedure TForm1.FormCreate(Sender : TObject); begin InitializeCriticalSection(CriticalSection); //初始化臨界區 end; procedure TForm1.FormDestroy(Sender : TObject); begin DeleteCriticalSection(CriticalSection); //刪除臨界區 end;
(2). 使用TcriticalSection 類
TcriticalSection 是在SyncObjs 單元中定義的類,要使用它需要先uses SyncObjs 。它對上
面的那些臨界區操作API 函數進行了封裝,簡化並方便了在Delphi 中的使用。例如
TcriticalSection.Enter 其實是調用了TRTLCriticalSection.Enter 。
使用TcriticalSection 類和一般類差不多,首先實例化TcriticalSection 類。使用的時候只要
在主線程當中創建這個臨界對象(注意一定要在需要同步的子線程之外建立這個對象)。
Tcriticalsection 類的構造函數比較簡單,沒有帶參數。
TcriticalSection.Enter 等效於EnterCriticalSection( ) 。
TcriticalSection.TryEnter 等效於TryEnterCriticalSection( ) 。
TcriticalSection.Leave 等效於LeaveCriticalSection( ) 。
例:
//在主線程中定義 var criticalsection : TCriticalsection; criticalsection := TCriticalsection.Create; … //在子線程中使用 criticalsection.Enter; try ... finally criticalsection.Leave; end;
警告:臨界區只有在所有的線程都使用它來訪問全局內存時才起作用,如果有線程直接調用
內存,而不通過臨界區,也會造成同時訪問的問題。
注意:臨界區主要是為實現線程之間同步的,但是使用的時候要注意,一定要在使用臨界區
同步的線程之外建立該臨界區(一般在主線程中定義臨界區並初始化臨界區)。臨界區是一
個進程里的所有線程同步的最好辦法,它不是系統級的,只是進程級的,也就是說它可能利
用進程內的一些標志來保證該進程內的線程同步,據Richter 說是一個記數循環。臨界區只
能在同一進程內使用。
2. Mutex 互斥
互斥是在序列化訪問資源時使用操作系統內核對象的一種方式。我們首先設置一個互斥對
象,然后訪問資源,最后釋放互斥對象。在設置互斥時,如果另一個線程(或進程)試圖設
置相同的互斥對象,該線程將會停下來,直到前一個線程(或進程)釋放該互斥對象為止。
注意它可以由不同應用程序共享。互斥的效果非常類似於臨界區,除了兩個關鍵的區別:首
先,互斥可用於跨進程的線程同步。其次,互斥對象能被賦予一個字符串名字,並且通過引
用此名字創建現有內核對象的附加句柄。線程同步使用臨界區,進程同步使用互斥。
當一個互斥對象不再被一個線程所擁有, 它就處於發信號狀態。此時首先調用
WaitForSingleObject( ) 函數(實現WaitFor 功能的API 還有幾個,這是最簡單的一個)的線
程就成為該互斥對象的擁有者,將互斥對象設為不發信號狀態。當線程調用ReleaseMutex( )
函數並傳遞一個互斥對象的句柄作為參數時,這種擁有關系就被解除,互斥對象重新進入發
信號狀態。
提示:臨界區和互斥的作用類似,都是用來進行同步的,但它們間有以下一點差別。臨界區
只能在進程內使用,也就是說只能是進程內的線程間的同步;而互斥則還可用在進程之間的;
臨界區隨着進程的終止而終止,而互斥,如果你不用CloseHandle( ) 的話,在進程終止后
仍然在系統內存在,也就是說它是操作系統全局內核對象;臨界區與互斥最大的區別是在性
能上,臨界區在沒有線程沖突時,要用10 ~ 15 個時間片,而互斥由於涉及到系統內核要用
400 ~ 600 個時間片;臨界區不是內核對象,它不由操作系統的低級部件管理,而且不能使
用句柄來操縱,而互斥屬於操作系統內核對象。
(1). 使用CreateMutex( ) API 函數
調用函數CreateMutex( ) 來創建一個互斥。下面是函數的聲明:
function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName:
PWideChar): THandle; stdcall;
lpMutexAttributes 參數為一個指向TsecurityAttributtes 記錄的指針。此參數通常設為nil ,
表示默認的安全屬性。bInitalOwner 參數表示創建互斥的線程是否要成為此互斥對象的初始
擁有者,當此參數為False 時,表示互斥對象沒有擁有者。lpName 參數指定互斥對象的名
稱,該名稱是大小寫區分的,設為nil 表示無命名,如果參數不是設為nil ,函數會搜索
是否有同名的互斥對象存在,如果有,函數就會返回同名互斥對象的句柄。否則,就新創建
一個互斥對象並返回其句柄。
當使用完互斥時,應當調用CloseHandle( ) 來關閉它。
WaitForSingleObject( ) 函數的使用:
在線程中使用WaitForSingleObject( ) 來防止其他線程進入同步區域的代碼。第一個調用
WaitForSingleObject( ) 函數的線程會將事件對象(不限於互斥對象)設為無信號狀態,其它線
程調用WaitForSingleObject( ) 函數時會檢查事件對象是否處於發信號狀態,這時狀態處於
無信號狀態,所以其它線程會掛起等待而不執行同步區域中的代碼。當第一個線程執行完同
步代碼后會釋放事件對象,事件對象重新進入發信號狀態並喚醒等待線程,其它線程會再次
將事件對象設為無信號狀態,防止另外的線程執行同步代碼。這就實現了線程同步。
此函數聲明如下:
function WaitForSingleObject(hHandle : THandle; dwMilliseconds : DWORD): DWORD; stdcall;
這個函數可以使當前線程在dwMilliseconds 參數指定的時間內等待事件對象信號,直到
hHandle 參數指定的事件對象進入發信號狀態為止。當一個事件對象不再被線程擁有時,它
就進入發信號狀態。當一個進程要終止時,它就進入發信號狀態。dwMilliseconds 參數設為
0 ,這意味着只檢查hHandle 參數指定的事件對象是否處於發信號狀態,而后立即返回該
信號狀態。dwMilliseconds 參數設為INFINITE ,表示如果信號不出現將一直等下去。
WaitForSingleObject( ) 在一個指定時間(dwMilliseconds)內等待一個事件對象變為有信號,
在此時間內,若等待的事件對象一直是無信號的,則調用線程將處於掛起狀態,否則繼續執
行。超過此時間后,線程繼續運行。
WaitForSingleObject( ) 函數返回值及含義:
WAIT_ABANDONED 指定的對象是一個事件對象,該對象沒有被擁有線程在線程結束前釋
放。此時就稱事件對象被拋棄。互斥對象的所有權被同意授予調用該函數的線程。互斥對象
被設置成為無信號狀態
WAIT_OBJECT_0 指定的對象處於發信號狀態
WAIT_TIMEOUT 等待的時間已過,對象仍然是非發信號狀態
WAIT_FAILED 語句出錯
WaitForMultipleObjects( ) 函數的使用:
WaitForMultipleObjects( ) 與WaitForSingleObject( ) 類似,只是它要么等待指定列表(由
lpHandles 指定)中若干個互斥對象(由nCount 決定)都變為有信號,要么等待一個列表
(由lpHandles 指定)中的一個對象變為有信號(由bWaitAll 決定)。該函數聲明如下:
function WaitForMultipleObjects(nCount: DWORD; lpHandles: PWOHandleArray; bWaitAll:
BOOL; dwMilliseconds: DWORD): DWORD; stdcall;
nCount 參數表示句柄的數量,最大值為MAXIMUM_WAIT_OBJECTS(64),lpHandles 參數
是指向句柄數組的指針,lpHandles 類型可以為(Event,Mutex,Process,Thread,Semaphore)
數組,bWaitAll 參數表示等待的類型,如果為True 則等待所有信號量有效再往下執行,設
為False 則當有其中一個信號量有效時就向下執行,dwMilliseconds 參數表示超時時間,超
時后向下繼續執行。
注意: 除WaitForSingleObject( ) 和WaitForMultipleObjects( ) 外, 你還可以使用
MsgWaitForMultipleObjects( ) 函數。該函數的詳細情況請看Win32 API 聯機文檔。
WaitForSingleObject( ) 不僅僅用於互斥,也用於信號量或事件,因此這里用詞為“事件對象”
而非互斥對象。在互斥例中,可以用互斥對象代替事件對象,同樣,在信號量例中,也能以
信號量對象代替事件對象。
再次提示,當一個互斥對象不再被一個線程所擁有,它就處於發信號狀態。此時首先調用
WaitForSingleObject( ) 函數的線程就成為該互斥對象的擁有者,此互斥對象設為無信號狀
態。當線程調用ReleaseMutex( ) 函數並傳遞一個互斥對象的句柄作為參數時,這種擁有關
系就被解除,互斥對象重新進入發信號狀態。ReleaseMutex( ) 聲明如下:
function ReleaseMutex(hMutex: THandle): BOOL; stdcall;
進程間需要同步時,只需要執行CreateMutex( ) 建立一個互斥對象,需要同步的時候只需
要WaitForSingleObject(mutexhandle, INFINITE) ,釋放時只需要ReleaseMutex(mutexhandle)
即可。
例: //先在主線程中創建互斥對象 var hMutex : THandle = 0;//定義一個句柄 ... hMutex := CreateMutex(nil, False, nil);//創建互斥對象,並返回其句柄 //在子線程的Execute 方法中加入以下代碼 WaitForSingleObject(hMutex, INFINITE);//互斥對象處於發信號狀態時進入同步區,否則等待 ... ReleaseMutex(hMutex); //最后記得要在主線程中釋放互斥對象 CloseHandle(hMutex);//關閉句柄
(2). 使用TMutex 類
TMutex 是在SyncObjs 單元中定義的類,其是ThandleObject 類的子類,要使用它需要先
uses SyncObjs 。它對上面的那些互斥操作API 函數進行了封裝,簡化並方便了在Delphi
中的使用。
使用前先實例化TMutex 類,其有多個重載的構造函數。聲明如下:
constructor Create(UseCOMWait: Boolean = False); overload; constructor Create(MutexAttributes: PSecurityAttributes; InitialOwner: Boolean; const Name: string; UseCOMWait: Boolean = False); overload; constructor Create(DesiredAccess: LongWord; InheritHandle: Boolean; const Name: string; UseCOMWait: Boolean = False); overload;
其實簡單的直接調用TMutex.Create 就可以返回一個TMutex 對象。
第一個版本將創建一個無名的、使用默認安全屬性、創建其的線程非互斥對象的初始擁有者
的TMutex 對象,其中的參數UseCOMWait 設為True 時表示當某個線程阻塞且等待互斥
對象時,任何單線程單元( STA ) COM 組件調用可以發回到該線程,其默認為False 。
第二個版本的MutexAttributes 參數通常設為nil 表示使用默認的安全屬性。InitialOwner 參
數表示創建線程是否是互斥對象的初始擁有者。Name 參數表示互斥對象的名字,大小寫區
分。
第三個版本的DesiredAccess 參數表示訪問互斥的方式,如果傳遞的訪問方式沒有被允許那
么構造函數會失敗,其參數可以是下面幾個常量的任意組合:
MUTEX_ALL_ACCESS, MUTEX_MODIFY_STATE, SYNCHRONIZE, _DELETE,
READ_CONTROL , WRITE_DAC , WRITE_OWNER 。但任何組合必須包含
SYNCHRONIZE 訪問權。InheritHandle 參數表示子進程是否可繼承該互斥對象句柄。
TMutex.Acquire 等效於WaitForSingleObject(mutexhandle, INFINITE) ,其實際上就是執行
THandleObject.WaitFor(INFINITE)。
TMutex.Release 實際上就是執行ReleaseMutex(mutexhandle)。
TMutex.Acquire 只能無限期等待一個互斥對象,要設置等待時間或等待多個互斥對象要使
用TMutex.WaitFor( ) 或TMutex.WaitForMultiple( )。
WaitFor( ) 是定義在TMutex 的父類ThandleObject 中的虛函數,聲明如下:
function WaitFor(Timeout: LongWord): TWaitResult; virtual;
其中返回值枚舉型TWaitResult 可以指示操作結果,wrSignaled 代表信號已set ,
wrTimeOut 代表超時且信號未set ,wrAbandoned 代表超時前事件對象被銷毀,wrError 代
表等待時出錯。
WaitForMultiple( ) 是定義在TMutex 的父類ThandleObject 中的類函數,聲明如下:
class function WaitForMultiple(const HandleObjs: THandleObjectArray; Timeout: LongWord;
AAll: Boolean; out SignaledObj: THandleObject; UseCOMWait: Boolean = False; Len: Integer =
0): TWaitResult;
其中HandleObjs 參數是包含了要等待的一系列事件對象的數組,AAll 參數設為True 時,
當所有事件對象都進入發信號狀態后該函數調用才會完成,當返回值為wrSignaled 且
AAll 參數設為False 時,第一個發信號的事件對象會被傳給SignaledObj 參數,Len 參數
設置監視事件對象的數量。
注意:WaitFor( ) 和WaitForMultiple( ) 均定義在ThandleObject 類中,而ThandleObject 類
是TMutex 、TSemaphore 、TEvent 類的父類,所以在描述WaitFor( ) 和WaitForMultiple( )
時使用的是事件對象而非互斥對象或信號量對象。
3. Semaphore 信號量
另一種使線程同步的技術是使用信號量對象。它是在互斥的基礎上建立的,它與互斥相似,
但它可以計數。信號量增加了資源計數的功能,預定數目的線程允許同時進入要同步的代碼。
例如可以允許一個給定資源同時被三個線程訪問。其實互斥就是最大計數為1 的信號量。
信號量的使用和互斥差不多。
(1). 使用CreateSemaphore( ) API 函數
可以用CreateSemaphore( ) 來創建一個信號量對象,其聲明如下:
function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes;
lInitialCount, lMaximumCount: Longint; lpName: PWideChar): THandle; stdcall;
和CreateMutex( ) 函數一樣, CreateSemaphore( ) 的第一個參數也是一個指向
TSecurityAttributes 記錄的指針,此參數的缺省值可以設為nil 。
lInitialCount 參數用來指定一個信號量的初始計數值,這個值必須在0 和lMaximumCount
之間。此參數大於0 ,就表示信號量處於發信號狀態。參數lMaximumCount 指定計數值
的最大值。如果這個信號量代表某種資源,那么這個值代表可用資源總數。
參數lpName 用於給出信號量對象的名稱,它類似於CreateMutex( ) 函數的lpName 參數。
在程序中使用WaitForSingleObject( ) 來防止其他線程進入同步區域的代碼。當調用
WaitForSingleObject( ) 函數( 或其他WaitFor 函數) 時, 此計數值就減1 。當調用
ReleaseSemaphore( ) 時,此計數值加1 ,此時同步區域代碼可以被其它線程訪問。其聲明
如下:
function ReleaseSemaphore(hSemaphore: THandle; lReleaseCount: Longint;
lpPreviousCount: Pointer): BOOL; stdcall;
其中hSemaphore 參數是創建的信號量句柄,lReleaseCount 參數是釋放時要增加的信號量
計數,lpPreviousCount 參數是通過該指針參數來獲得釋放前的信號量計數,如果不用設為
nil 。
當使用完信號量時,應當調用CloseHandle( ) 來關閉它。
注意:一般的同步使用互斥,是因為其有一個特別之處,當一個持有互斥的線程DOWN 掉
的時候,互斥可以自動讓其它等待這個對象的線程接受,而其它的內核對象則不具體這個功
能。之所以要使用信號量則是因為其可以提供一個活動線程的上限,即lMaximumCount 參
數,這才是它的真正有用之處。
例:
var Form1 : TForm1; HSem : THandle = 0;//定義一個信號量 implementation var tick : Integer = 0; procedure TMyThread.Execute; var WaitReturn : DWord ; begin WaitReturn := WaitForSingleObject(HSem, INFINITE);//使用信號量對象,信號量減1 Form1.Edit1.Text := IntToStr(tick); Inc(tick); Sleep(10); ReleaseSemaphore(HSem, 1, Nil);//釋放信號量對象,信號量加1 end; … procedure TForm1.FormCreate(Sender: TObject); begin HSem := CreateSemaphore(Nil, 1, 1, Nil);//創建信號量對象 end; procedure TForm1.FormDestroy(Sender: TObject); begin CloseHandle(HSem);//銷毀信號量 end; procedure TForm1.Button1Click(Sender: TObject); var index : Integer; begin for index := 0 to 10 do TMyThread.Create; end;
(2). 使用TSemaphore 類
TSemaphore 是在SyncObjs 單元中定義的類,其是ThandleObject 類的子類,要使用它需
要先uses SyncObjs 。它對上面的API 函數進行了封裝,簡化並方便了在Delphi 中的使
用。
其有三個版本的構造器,簡單執行TSemaphore.Create 就可實例化一個對象:
constructor Create(UseCOMWait: Boolean = False); overload; constructor Create(SemaphoreAttributes: PSecurityAttributes; AInitialCount: Integer; AMaximumCount: Integer; const Name: string; UseCOMWait: Boolean = False); overload; constructor Create(DesiredAccess: LongWord; InheritHandle: Boolean; const Name: string; UseCOMWait: Boolean = False); overload;
參數參見上面介紹。
TSemaphore.Acquire 等效於WaitForSingleObject(semaphorehandle, INFINITE) ,其實際上就
是執行THandleObject.WaitFor(INFINITE)。或者使用WaitFor( ) 和WaitForMultiple( ) 函數,
這兩個函數可以設置等待的時間或等待多個事件對象。
TSemaphore.Release 有兩個版本,聲明如下:
procedure Release; override; overload;
function Release(AReleaseCount: Integer): Integer; overload; reintroduce;
第一個版本實際執行ReleaseSemaphore(FHandle, 1, nil)
第二個版本AReleaseCount 參數表示釋放時增加的信號量計數值,返回值是釋放前的信號
量計數值。實際執行ReleaseSemaphore(FHandle, AReleaseCount, @Result),其中@Result 是
指向Release 函數返回值Integer 類型的指針。如果要指定增加計數值應使用第二個版本。
4. Event 事件
事件( Event )與Delphi 中的事件有所不同。從本質上說,Event 其實相當於一個全局的布
爾變量。它有兩個賦值操作: SetEvent 和ResetEvent ,相當於把它設置為True 或False 。
而檢查它的值是通過WaitForSingleObject( ) (或其它WaitFor 函數)操作進行。SetEvent 和
ResetEvent 操作是原語操作,所以Event 可以實現一般布爾變量不能實現的在多線程中的
應用。
當Event 從Reset 狀態向Set 狀態轉換時,喚醒其它掛起的線程,這就是它為什么叫
Event 的原因。所謂“事件”就是指“狀態的轉換”。通過Event 可以在線程間傳遞這種“狀
態轉換”信息。所以其本質是用來通知某事已經發生的信號,在這里可用來表示共享資源已
經在使用或已經使用完的信號。
(1). 使用CreateEvent( ) API 函數
使用CreateEvent( ) 創建一個事件,聲明如下:
function CreateEvent(lpEventAttributes: PSecurityAttributes;
bManualReset, bInitialState: BOOL; lpName: PWideChar): THandle; stdcall;
其中bManualReset 參數代表創建的Event 是自動復位還是人工復位,如果設為True 表示
人工復位,一旦該Event 被設置為有信號,則它一直會等到手動執行ResetEvent( ) 時才會
變為無信號,設為False 表示自動復位,Event 被設置為有信號時,則當有一個線程執行
WaitForSingleObject( ) 時該Event 就會自動復位,變成無信號。bInitialState 參數代表事件
的初始狀態,設為True,事件創建后為有信號,設為False 則為無信號。
不同於互斥或信號量,Event 不使用Release 相關函數設置相關對象進入發信號狀態,而使
用SetEvent( ) 函數,當線程執行完同步代碼要從同步區域中離開時應執行該函數,聲明如
下:
function SetEvent(hEvent: THandle): BOOL; stdcall;
當事件創建為人工復位時,在線程進入同步區域執行同步代碼前應執行ResetEvent( ) 函數,
將Event 設為無信號。聲明如下:
function ResetEvent(hEvent: THandle): BOOL; stdcall;
PulseEvent( ) 是一個比較有意思的方法,正如名字,它使一個Event 對象的狀態發生一次
脈沖變化,將無信號設為有信號,喚醒等待的線程,再設為無信號,而整個操作是原子的。
對自動復位的Event 對象,它僅喚醒第一個等到該事件的線程(如果有的話),而對於人工復
位的Event 對象,它喚醒所有等待的線程。聲明如下:
function PulseEvent(hEvent: THandle): BOOL; stdcall;
當使用完事件時,應當調用CloseHandle( ) 來關閉它。
(2). 使用TEvent 類
TEvent 是在SyncObjs 單元中定義的類,其是ThandleObject 類的子類,要使用它需要先
uses SyncObjs 。它對上面的API 函數進行了封裝,簡化並方便了在Delphi 中的使用。
TEvent 若在多線程環境中可用於與其它線程同步;若在單線程環境中可用於調整響應不同
異步事件(如系統消息或用戶動作)的代碼段。構造函數如下:
constructor Create(EventAttributes: PSecurityAttributes; ManualReset: Boolean;
InitialState: Boolean; const Name: string; UseCOMWait: Boolean = False); overload;
constructor Create(UseCOMWait: Boolean = False); overload;
ManualReset 參數為是否手工復位,InitialState 參數為初始狀態。
TEvent.SetEvent( ) 和TEvent.ResetEvent( ) 均無參數。
TEvent 類中沒有定義與PulseEvent 功能一樣的方法。
TEvent 類同樣可以使用WaitFor( ) 和WaitForMultiple( ) 函數。
但要注意的是,TEvent 類並沒有實現Acquire 函數,該函數是定義在TSynchroObject 類
中僅作為接口、沒有執行代碼的虛函數。TSynchroObject 是ThandleObject 類的父類。其實
自己實現Acquire 函數也不難,它實際上是執行THandleObject.WaitFor(INFINITE) 函數,
仿照上面的TMutex 類寫就可以。
另外,Delphi 中定義了一個更簡單的事件類,TSimpleEvent 類,但從源代碼上看,該類僅
有TSimpleEvent = class(TEvent); 一句,並未定義任何屬於TSimpleEvent 的成員。估計是
作為向后兼容而存在。
5. Global Atom 全局原子
Windows 系統中,為了實現信息共享,系統維護了一張全局原子表( Global Atom Table ),
用於保存字符串與之對應的標志符(原子)的組合,系統能保證其中的每個原子都是唯一的,
管理其引用計數,並且當該全局原子的引用計數為0 時,從內存中清除。應用程序在原子表
中可以放置字符串,並接收一個16 位整數值(叫做原子,即Atom ),它可以用來提取該字
符串。放在原子表中的字符串叫做原子的名字。系統提供了許多原子表。每個表有不同的目
的。例如,動態數據交換( DDE )應用程序使用全局原子表與其他應用程序共享項目名稱和
主題名稱字符串,不傳遞實際的字符串,一個DDE 應用程序傳遞全局原子給它的父進程,
父進程使用原子提取原子表中的字符串,這就是利用全局原子進行進程或線程間的數據交
換;使用全局原子也可防止多次啟動某個程序。
應用程序可以使用本地原子表來有效地管理大量只用於程序內部的字符串。這些字符串,以
及相關聯的原子,只對創建該原子表的應用程序可用。一個在許多數據結構中需要相同字符
串的應用程序,可以通過使用本地原子表來減少內存使用。程序可以把字符串放入原子表,
把相關的原子放入結構,而無需把字符串拷到每個結構中。這樣,一個字符串在內存中只出
現一次,但可以在程序中多次使用。應用程序也可以使用本地原子表來快速搜索特定的字符
串。要實現這樣的搜索,程序只需把要搜索的字符串放入原子表中,然后把結果原子與相關
數據結構中的原子相比較。通常情況下,比較原子要比比較字符串要快得多。原子表是用哈
希表實現的。默認時,一個本地原子表使用37 個bucket 的哈希表。不過,你可以通過調
用InitAtomTable 函數來改變bucket 數量。如果程序准備調用InitAtomTable ,那它必須
在調用任何其他原子管理函數前調用它。這里只簡單介紹本地原子表。它有多個相關的函數,
function InitAtomTable(nSize: DWORD): BOOL; stdcall; function DeleteAtom(nAtom: ATOM): ATOM; stdcall; function AddAtom(lpString: PWideChar): ATOM; stdcall; function FindAtom(lpString: PWideChar): ATOM; stdcall; function GetAtomName(nAtom: ATOM; lpBuffer: PWideChar; nSize: Integer): UINT; stdcall;
以下介紹全局原子表相關函數。
function GlobalAddAtom(lpString: PWideChar): ATOM; stdcall;
增加一個字符串到全局原子表中,並返回一個唯一標識值。
lpString 參數為要添加到全局原子表中的字符串。
如果成功返回新增加的全局原子,失敗則返回0 。ATOM 類型等於Word 類型。
function GlobalDeleteAtom(nAtom: ATOM): ATOM; stdcall;
減少對指定全局原子的引用計數,引用計數減1 ,如果引用計數為零,系統會在全局原子
表中刪除此原子。
此函數一直返回0 。
只要全局原子的引用計數大於0 ,其原子名稱將保留在全局原子表中,即使把它放入表中
的應用程序終結了。一個本地的原子表在應用程序終結時被銷毀,而不管其中原子的引用計
數是多少。
function GlobalFindAtom(lpString: PWideChar): ATOM; stdcall;
在全局原子表中查找是否存在指定字符串。
lpString 參數為要查找的字符串。
如果在全局原子表中存在要查找的字符串,則返回此字符串對應的原子,沒有找到則返回0。
function GlobalGetAtomName(nAtom: ATOM;
lpBuffer: PWideChar; nSize: Integer): UINT; stdcall;
返回指定原子所對應的字符串。
nAtom 參數為指定查找的原子,lpBuffer 參數為要存放字符串的緩沖區,nSize 參數為緩沖
區大小。
若操作成功返回緩沖區接受長度,若失敗返回0 。UINT 類型等於LongWord 類型。
例:
//在程序的program 文件中 ... if GlobalFindAtom(iAtom) = 0 then begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end else MessageBox(0, '已經有一個程序在運行', ' ', mb_OK); ...
6. Synchronize 同步
Synchronize( ) 是定義在TThread 類中的函數,它可以讓要執行的代碼實現線程同步,但這
種同步其實是偽同步,其原理是將子線程要執行的代碼通過消息傳遞給主線程,由主線程來
執行,主線程將代碼放在一個隱蔽的窗口里運行,而子線程會等待主線程將執行結果發給它,
這樣的話,這段代碼就不是子線程代碼,而是一般的主線程代碼。Synchronize( ) 只是將該
線程的代碼放到主線程中運行,並非實際意義的線程同步。RAD Studio VCL Reference 中也
描述為:Executes a method call within the main thread,Synchronize causes the call specified by
AMethod(參數) to be executed using the main thread,,thereby avoiding multi-thread conflicts。
這里有一個問題,如果Synchronize( ) 執行的代碼很繁忙,例如執行的代碼運算過於復雜、
龐大或者從數據庫中取出大量數據,數據庫不會立即返回數據時或者使用ADO 組件連接
數據庫,而這時數據庫無法連接,ADO 組件需要超時才會終止運行,這些都會導致主窗口
會阻塞掉,看似死機一般。因此,通常對用戶界面類VCL 組件的訪問才使用Synchronize( )
函數,一般用戶界面類VCL 組件都由主線程創建、存在於主窗口中,而且對VCL 組件的
訪問或修改的執行效率都比較高,不會過多的影響性能。絕對不能在主線程中執行
Synchronize( ) 函數,這會導致無限循環。
Synchronize( ) 函數一般在線程的Execute 函數中調用。其有四個版本,兩個是類函數,兩
個是靜態函數,聲明如下:
class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload; class procedure Synchronize(AThread: TThread; AThreadProc: TThreadProcedure); overload; procedure Synchronize(AMethod: TThreadMethod); overload; procedure Synchronize(AThreadProc: TThreadProcedure); overload;
AThread 參數是當前線程,TThreadMethod 是對象的函數指針類型,TThreadProcedure 是匿
名函數類型。
注意:Synchronize( ) 的AMethod 或AThreadProc 參數必須是一個無參數的procedure ,
故在此procedure 中無法傳遞參數值,通常的解決方法是在線程類中增加額外的成員,用其
代替參數來傳遞信息。
例:
type TMyThread = class(TThread) str : string;//額外的域,代替參數將字符串寫入Memo ... procedure TMyThread.WriteMemo; begin Memo.Lines.Add(str); end; ... procedure TMyThread.Execute; begin str := 'Hello'; synchronize(WriteMemo); end;