Delphi 線程同步技術(轉)

上次跟你們分享了線程的標準代碼,其實在線程的使用中最重要的是線程的同步問題,若是你在使用線程後,發現你的界面常常被卡死,或者沒法顯示出來,顯示混亂,你的使用的變量值總是不按預想的變化,結果每每出乎意料,那麼你頗有多是忽略了線程同步的問題。

當有多個線程的時候,常常須要去同步這些線程以訪問同一個數據或資源。例如,假設有一個程序,其中一個線程用於把文件讀到內存,而另外一個線程用於統計文件中的字符數。固然,在把整個文件調入內存以前,統計它的計數是沒有意義的。可是,因爲每一個操做都有本身的

線程,操做系統會把兩個線程看成是互不相干的任務分別執行,這樣就可能在沒有把整個文

件裝入內存時統計字數。爲解決此問題,你必須使兩個線程同步工做。存在一些線程同步地

址的問題,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 就可實例化一個對象:

ide

複製代碼
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( ) 來關閉它。post



(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;
複製代碼
相關文章
相關標籤/搜索