Delphi執行緒同步(臨界區、互斥、訊號量)
當有多個執行緒的時候,經常需要去同步這些執行緒以訪問同一個資料或資源。
例如,假設有一個程式,其中一個執行緒用於把檔案讀到記憶體,而另一個執行緒用於統計檔案的字元數。當然,在整個檔案調入記憶體之前,統計它的計數是沒有意義的。但是,由於每個操作都有自己的執行緒,作業系統會把兩個執行緒當做是互不相干的任務分別執行,這樣就可能在沒有把整個檔案裝入記憶體時統計字數。為解決此問題,你必須使兩個執行緒同步工作
存在一些執行緒同步地址的問題,Win 32 提供了許多執行緒同步的方式。這裡將會講到:臨界區、互斥、訊號量和事件
為了檢驗這些技術,首先來看一個需要執行緒同步解決的問題。假設有一個整數陣列,需要按照升序賦初值。現在要在第一遍把這個陣列賦初值為1~128,第二遍將此陣列賦初值為128~255,然後結果顯示在列表中。要用兩個執行緒來分別進行初始化。下面的程式碼給出了沒有做執行緒同步的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
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()過程初始化它,其宣告如下
?1 |
procedure
InitializeCriticalSection( var
lpCriticalSection: TRLCriticalSection); stdcall;
|
lpCriticalSection引數是一個TRTLCriticalSection型別的記錄,並且是變參。至於TRTLCriticalSection是如何定義的,這並不重要,因為很少需要檢視這個記錄中的具體內容。只需要在lpCriticalSection中傳遞為初始化的記錄,InitializeCriticalSection()過程就會填充這個記錄
注意:Microsoft 故意隱瞞了TRTLCriticalSection 的細節。因為,其內容在不同的硬體平臺上是不同的。在基於Intel 的平臺上,TRTLCriticalSection 包含一個計數器、一個指示當前執行緒控制代碼的域和一個系統事件的控制代碼。在Alpha 平臺上,計數器被替換為一種Alpha-CPU 資料結構,稱為spinlock。
在記錄被填充之後,我們就可以開始建立臨界區了。這是我們需要使用EnterCriticalSection() 和LeaveCriticalSection() 來封裝程式碼塊。這兩個過程的宣告如下
?1 2 3 |
procedure
EnterCriticalSection( var
lpCriticalSection: TRTLCriticalSection); stdcall;
procedure
LeaveCriticalSection( var
lpCriticalSection: TRTLCriticalSection); stdcall;
|
正如你所想的,引數 lpCriticalSection 就是有InitializeCriticalSection() 填充的記錄
當你不需要TRTLCriticalSection 記錄時,應當呼叫 DeleteCriticalSection() 過程,下面是它的宣告
?1 |
procedure
DeleteCriticalSection( var
lpCriticalSection: TRTLCriticalSection); stdcall;
|
下面演示利用臨界區來同步陣列初始化執行緒的技術
?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
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() 來建立一個互斥量。下面是函式的宣告
?1 |
function
CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName:
PChar ): THandle; stdcall;
|
lpMutexAttributes 引數為一個指向TSecurityAttributes記錄的指標。此引數通常設為nil , 表示預設的安全屬性
bInitalOwner 引數表示建立互斥物件執行緒是否稱為互斥物件的擁有者。當此引數為False時,表示互斥物件沒有擁有者。
lpName 引數指定互斥物件的名稱。設為nil表示無命名,如果引數不設為nil,函式會搜尋是否有同名的互斥物件存在。如果有,函式就會返回同名互斥物件的控制代碼。否則,就新建立一個互斥物件並返回其控制代碼。
當使用完互斥物件時,應當呼叫CloseHandle()來關閉它。
下面演示使用互斥技術來使兩個程序對一個數組的初始化同步
?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
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() 來防止其他程序進入同步區域的程式碼。此函式宣告如下
?1 |
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() 來建立一個訊號量物件,其宣告如下
?1 |
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 引數。
下面是使用訊號量技術來同步初始化陣列的程式碼
?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
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
|