當有多個線程的時候,經常需要去同步這些線程以訪問同一個數據或資源。
例如,假設有一個程序,其中一個線程用於把文件讀到內存,而另一個線程用於統計文件的字符數。當然,在整個文件調入內存之前,統計它的計數是沒有意義的。但是,由於每個操作都有自己的線程,操作系統會把兩個線程當做是互不相干的任務分別執行,這樣就可能在沒有把整個文件裝入內存時統計字數。為解決此問題,你必須使兩個線程同步工作
存在一些線程同步地址的問題,Win 32 提供了許多線程同步的方式。這里將會講到:臨界區、互斥、信號量和事件
為了檢驗這些技術,首先來看一個需要線程同步解決的問題。假設有一個整數數組,需要按照升序賦初值。現在要在第一遍把這個數組賦初值為1~128,第二遍將此數組賦初值為128~255,然后結果顯示在列表中。要用兩個線程來分別進行初始化。下面的代碼給出了沒有做線程同步的代碼
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMainForm =class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end; TFooThread = class(TThread) protected procedure Execute; override; end; var MainForm: TMainForm; implementation {$R *.DFM} const MaxSize = 128; var NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer; function GetNextNumber: Integer; begin Result:= NextNumber; //return global var Inc(NextNumber); //inc global var end; procedure TFooThread.Execute; var i: Integer; begin OnTerminate:= MainForm.ThreadsDone; for i:=1 to MazSize do begin GlobalArray[i]:= GetNextNumber; Sleep(5); end; end; procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin Inc(DoneFlags); if DoneFlags = 2 then for i:=1 to MaxSize do ListBox1.Items.Add(IntToStr(GlobalArray[i])); //注意ListBox的使用,並看下面編譯運行的效果圖 end; procedure TMainForm.Button1Click(Sender: TObject); begin TFooThread.Create(False); //創建一個新的線程 TFooThread.Create(Flase); //再創建一個新的線程 end; end.
因為兩個線程同時運行,同一個數組在兩個線程中被初始化會出現什么呢?你可以看下面的截圖
這個問題的解決方案是:當兩個線程訪問這個全局數組時,為防止它們同時執行,需要使用線程的同步。這樣,你就會得到一組合理的數值
1.臨界區
臨界區是一種最直接的線程同步方法。所謂臨界區,就是一次只能有一個線程來執行的一段代碼。如果把初始化數組的代碼放在臨界區內,那么另一個線程在第一個線程處理完之前是不會被執行的。
在使用臨界區之前,必須使用 InitializeCriticalSection()過程初始化它,其聲明如下
procedure InitializeCriticalSection(var lpCriticalSection: TRLCriticalSection); stdcall;
lpCriticalSection參數是一個TRTLCriticalSection類型的記錄,並且是變參。至於TRTLCriticalSection是如何定義的,這並不重要,因為很少需要查看這個記錄中的具體內容。只需要在lpCriticalSection中傳遞為初始化的記錄,InitializeCriticalSection()過程就會填充這個記錄
注意:Microsoft 故意隱瞞了TRTLCriticalSection 的細節。因為,其內容在不同的硬件平台上是不同的。在基於Intel 的平台上,TRTLCriticalSection 包含一個計數器、一個指示當前線程句柄的域和一個系統事件的句柄。在Alpha 平台上,計數器被替換為一種Alpha-CPU 數據結構,稱為spinlock。
在記錄被填充之后,我們就可以開始創建臨界區了。這是我們需要使用EnterCriticalSection() 和LeaveCriticalSection() 來封裝代碼塊。這兩個過程的聲明如下
procedure EnterCriticalSection(var lpCriticalSection: TRTLCriticalSection); stdcall; procedure LeaveCriticalSection(var lpCriticalSection: TRTLCriticalSection); stdcall;
正如你所想的,參數 lpCriticalSection 就是有InitializeCriticalSection() 填充的記錄
當你不需要TRTLCriticalSection 記錄時,應當調用 DeleteCriticalSection() 過程,下面是它的聲明
procedure DeleteCriticalSection(var lpCriticalSection: TRTLCriticalSection); stdcall;
下面演示利用臨界區來同步數組初始化線程的技術
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMainForm =class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end; TFooThread = class(TThread) protected procedure Execute; override; end; var MainForm: TMainForm; implementation {$R *.DFM} const MaxSize = 128; var NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer; CS: TRTLCriticalSection; // function GetNextNumber: Integer; begin Result:= NextNumber; //return global var Inc(NextNumber); //inc global var end; procedure TFooThread.Execute; var i: Integer; begin OnTerminate:= MainForm.ThreadsDone; EnterCriticalSection(CS); // for i:=1 to MazSize do begin GlobalArray[i]:= GetNextNumber; Sleep(5); end; LeaveCriticalSection(CS); // end; procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin Inc(DoneFlags); if DoneFlags = 2 then for i:=1 to MaxSize do ListBox1.Items.Add(IntToStr(GlobalArray[i])); DeleteCriticalSection(CS); // end; end; procedure TMainForm.Button1Click(Sender: TObject); begin InitializeCriticalSection(CS); // TFooThread.Create(False); //創建一個新的線程 TFooThread.Create(Flase); //再創建一個新的線程 end; end.
在第一個線程調用EnterCriticalSection()之后,所有別的線程就不能再進入代碼塊。下一個線程要等到第一個線程調用LeaveCriticalSection()之后才能被喚醒,輸出結果顯示如下
2.互斥
互斥非常類似於臨界區,除了兩個關鍵的區別:
1)首先,互斥可用於跨進程的線程同步
2)其次,互斥能被賦予一個字符串名字,並且通過引用此名字創建現有互斥對象的附加句柄
提示:臨界區與事件對象(比如互斥對象)的最大的區別在性能上。臨界區在沒有線程沖突時,要用10~15個時間片,而事件對象由於涉及到系統內核,所以要用400~600個時間片
可以調用函數CreatMutex() 來創建一個互斥量。下面是函數的聲明
function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName: PChar): THandle; stdcall;
lpMutexAttributes 參數為一個指向TSecurityAttributes記錄的指針。此參數通常設為nil , 表示默認的安全屬性
bInitalOwner 參數表示創建互斥對象線程是否稱為互斥對象的擁有者。當此參數為False時,表示互斥對象沒有擁有者。
lpName 參數指定互斥對象的名稱。設為nil表示無命名,如果參數不設為nil,函數會搜索是否有同名的互斥對象存在。如果有,函數就會返回同名互斥對象的句柄。否則,就新創建一個互斥對象並返回其句柄。
當使用完互斥對象時,應當調用CloseHandle()來關閉它。
下面演示使用互斥技術來使兩個進程對一個數組的初始化同步
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMainForm =class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end; TFooThread = class(TThread) protected procedure Execute; override; end; var MainForm: TMainForm; implementation {$R *.DFM} const MaxSize = 128; var NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer; hMutex: THandle = 0; // function GetNextNumber: Integer; begin Result:= NextNumber; //return global var Inc(NextNumber); //inc global var end; procedure TFooThread.Execute; var i: Integer; begin FreeOnTerminate:= True; OnTerminate:= MainForm.ThreadsDone; if WaitForSingleObject(hMutex, INFINITE) = WAIT_OBJECT_0 then // begin for i:=1 to MazSize do begin GlobalArray[i]:= GetNextNumber; Sleep(5); end; end; ReleaseMutex(hMutex); // end; procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin Inc(DoneFlags); if DoneFlags = 2 then for i:=1 to MaxSize do ListBox1.Items.Add(IntToStr(GlobalArray[i])); CloseHandle(hMutex); // end; end; procedure TMainForm.Button1Click(Sender: TObject); begin hMutex:= CreateMutex(nil, False, nil); // TFooThread.Create(False); //創建一個新的線程 TFooThread.Create(Flase); //再創建一個新的線程 end; end.
你將注意到,在程序中使用 WaitForSingleObject() 來防止其他進程進入同步區域的代碼。此函數聲明如下
function WaitForSingleObject(hHandle: Thandle; dwMilliseconds: DWORD): DWORD; stdcall;
這個函數可以使當前線程在dwMilliseconds 指定的時間內睡眠,直到 hHandle參數指向的對象進入發信號狀態為止。一個互斥對象不再被線程擁有時,它就進入發信號狀態。當一個進程要終止時,它就進入發信號狀態,而后立即返回。dwMilliSeconds參數設為 INFINITE,表示如果信號不出現將一直等下去。這個函數的返回值列在下表
返回值 | 含義 |
WAIT_ABANDONED | 指定的對象時互斥對象,並且擁有這個互斥對象的線程在沒有釋放此對象之前就已經終止。此時就稱互斥對象被拋棄。這種情況下,這個互斥對象歸當前線程所有,並把它設為非發信號狀態 |
WAIT_OBJECT_0 | 指定的對象處於發信號狀態 |
WAIT_TIMEOUT | 等待的事件已過,對象仍然是非發信號狀態 |
再次聲明,當一個互斥對象不再被一個線程所擁有,它就處於發信號狀態,此時首先調用WaitForSignalObject() 函數的線程就稱為該互斥對象的擁有者,此互斥對象設為不發信號狀態。當線程調用ReleaseMutex() 函數並傳遞一個互斥對象的句柄作為參數時,這種擁有關系就被解除,互斥對象重新進入發信號狀態
注意 除WaitForSingleObject() 函數外,你還可以使用 WaitForMultipleObject() 和MsgWaitForMultipleObject() 函數,它們可以等待幾個對象變為發信號狀態。這兩個函數的詳細情況請看Win32 API聯機文檔
3.信號量
另外一種使線程同步的技術是使用信號量對象。它是在互斥的基礎上建立的,但是信號量增加了資源計數的功能,預定數目的線程允許同時進入要同步的代碼。可以用 CreateSemaphore() 來創建一個信號量對象,其聲明如下
function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes; lInitialCount, lMaxiMumCount: LongInt; lpName: PChar): THandle; stdcall;
和CreateMutex() 函數一樣,CreateSemaphore() 的第一個參數也是一個指向 TSecurityAttributes 記錄的指針,此參數的缺省值可以設為 nil。
lInitialCount 參數用來指定一個信號量的初始計數值,這個值必須在 0 和 lMaximumCount 之間。此參數大於 0,就表示信號量處於發信號狀態。當調用 WaitForSingleObject() 函數(或其他函數)時,此計數值就減1。當調用 ReleaseSemaphore() 時,此計數值加1。
參數 lMaximumCount 指定計數值的最大值。如果這個信號量代表某種資源,那么這個值代表可用資源總數
參數 lpName 用於給出信號量對象的名稱,它類似於 CreateMutex() 函數的 lpName 參數。
下面是使用信號量技術來同步初始化數組的代碼
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMainForm =class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end; TFooThread = class(TThread) protected procedure Execute; override; end; var MainForm: TMainForm; implementation {$R *.DFM} const MaxSize = 128; var NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer; hSem: THandle = 0; // function GetNextNumber: Integer; begin Result:= NextNumber; //return global var Inc(NextNumber); //inc global var end; procedure TFooThread.Execute; var i: Integer; WaitReturn: DWORD; begin OnTerminate:= MainForm.ThreadsDone; WaitReturn:= WaitForSingleObject(hSem, INFINITE); // if WaitReturn = WAIT_OBJECT_0 then // begin for i:=1 to MazSize do begin GlobalArray[i]:= GetNextNumber; Sleep(5); end; end; ReleaseSemaphore(hSem, 1, nil); // end; procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin Inc(DoneFlags); if DoneFlags = 2 then for i:=1 to MaxSize do ListBox1.Items.Add(IntToStr(GlobalArray[i])); CloseHandle(hSem, ); // end; end; procedure TMainForm.Button1Click(Sender: TObject); begin hSem:= CreateSemaphore(nil, 1, 1 , nil); // TFooThread.Create(False); //創建一個新的線程 TFooThread.Create(Flase); //再創建一個新的線程 end; end.
在這個程序中因為只允許一個線程進入要同步的代碼,所以信號量的最大計數值(lMaximumCount)要設為1。
ReleaseSemaphore() 函數將使信號量對象的計數加 1。請注意此函數比 ReleaseMutex() 更復雜。ReleaseSemaphore() 函數聲明如下:
function ReleaseSemaphore(hSemaphore: Thandle; lReleaseCount: LongInt; lpPreviousCount: Pointer): BOOL; stdcall;
lReleaseCount 數用於指定每次使計數值加多少。如果參數 lpPreviousCount 不為nil ,原有的計數值將存儲在 lpPreviousCount 里。信號量對象並不屬於某個線程。例如,假設一個信號量對象的最大計數值是10,並且有10個線程調用 WaitForSingleObject() ,計數值將減至0。只要有一個線程調用 ReleaseSemaphore(),並且把lReleaseCount參數設為10。這時,計數值又恢復為10,同時10個線程又恢復發信號狀態。不過,此函數使調試變得困難,使用時要小心。
記住,最后一定要調用 CloseHandle() 函數來釋放由CreateSemaphore()創建的信號量對象的句柄。
最后說一下Application.ProcessMessages
順便總結Application.ProcessMessages的作用:運行一個非常耗時的循環,那么在這個循環結束前,程序可能不會響應任何事件,按鈕沒有反應,程序設置無法繪制窗體,看上去就如同死了一樣,這有時不是很方便,例如於終止循環的機會都沒有了,又不想使用多線程時,這時你就可以在循環中加上這么一句,每次程序運行到這句時,程序就會讓系統響應一下消息,從而使你有機會按按鈕,窗體有機會繪制。所起作用類似於VB中DoEvent方法. 調用ProcessMessages來使應用程序處於消息隊列能夠進行消息處理,ProcessMessages將Windows消息進行循環輪轉,直至消息為空,然后將控制返回給應用程序。
注示:僅在應用程序調用ProcessMessages時勿略消息進程效果,而並非在其他應用程序中。在冗長的操作中,調用ProcessMessages周期性使得應用程序對畫筆或其他信息產生回應。 ProcessMessages不充許應該程序空閑,而HandleMessage則然.使用ProcessMessages一定要保證相關代碼是可重入的,如果實在不行也可按我上面的方法實現同步。