Delphi線程同步(臨界區、互斥、信號量)


  當有多個線程的時候,經常需要去同步這些線程以訪問同一個數據或資源。

  例如,假設有一個程序,其中一個線程用於把文件讀到內存,而另一個線程用於統計文件的字符數。當然,在整個文件調入內存之前,統計它的計數是沒有意義的。但是,由於每個操作都有自己的線程,操作系統會把兩個線程當做是互不相干的任務分別執行,這樣就可能在沒有把整個文件裝入內存時統計字數。為解決此問題,你必須使兩個線程同步工作

  存在一些線程同步地址的問題,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一定要保證相關代碼是可重入的,如果實在不行也可按我上面的方法實現同步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM