(彙)Delphi+MultiThread

關于線程同步的一些方法

線程是進程內一個相對獨立的、可調度的執行單元。一個應用可以有一個主線程,一個主線程可以有多個子線程,子線程還可以有自己的子線程,這樣就構成了多線程應用了。由于多個線程往往會同時訪問同一塊內存區域,頻繁的訪問這塊區域,將會增加産生線程沖突的概率。一旦産生了沖突,將會造成不可預料的結果(該公用區域的值是不可預料的)可見處理線程同步的必要性。

注意:本文中出現的所有代碼都是用DELPHI描述的,調試環境爲Windows me ,Delphi 6。其中所涉及的Windows API函數可以從MSDN獲得詳細的文檔。

首先引用一個實例來引出我們以下的討論,該實例沒有采取任何措施來避免線程沖突,它的主要過程爲:由主線程啓動兩個線程對letters這個全局變量進行頻繁的讀寫,然後分別把修改的結果顯示到ListBox中。由于沒有同步這兩個線程,使得線程在修改letters時産生了不可預料的結果。

ListBox中的每一行的字母都應該一致,但是上圖畫線處則不同,這就是線程沖突産生的結果。當兩個線程同時訪問該共享內存時,一個線程還未對該內存修改完,另一個線程又對該內存進行了修改,由于寫值的過程沒有被串行化,這樣就産生了無效的結果。可見線程同步的重要性。

以下是本例的代碼

unit.pas文件

unit Unit1;

interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls;

//定義窗口類

type

TForm1 = class(TForm)

ListBox1: TListBox;

ListBox2: TListBox;

Button1: TButton;

procedure Button1Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

//定義線程類

type

TListThread=class(TThread)

private

Str:String;

protected

procedure AddToList;//將Str加入ListBox組件

Procedure Execute;override;

public

LBox:TListBox;

end;

//定義變量

var

Form1: TForm1;

Letters:String='AAAAAAAAAAAAAAAAAAAA';//全局變量

implementation

{$R *.dfm}

//線程類實現部分

procedure TListThread.Execute;

var

I,J,K:Integer;

begin

for i:=0 to 50 do

begin

for J:=1 to 20 do

for K:=1 to 1000 do//循環1000次增加産生沖突的幾率

if letters[j]<'Z' then

letters[j]:=succ(Letters[j])

else

letters[j]:='A';

str:=letters;

synchronize(addtolist);//同步訪問VCL可視組件

end;

end;

procedure TListThread.AddToList;

begin

LBox.Items.Add(str);//將str加入列表框

end;

//窗口類實現部分

procedure TForm1.Button1Click(Sender: TObject);

var

th1,th2:TListThread;

begin

Listbox1.Clear;

Listbox2.Clear;

th1:=tlistThread.Create(true);//創建線程1

th2:=tlistThread.Create(true);//創建線程2

th1.LBox:=listBox1;

th2.LBox:=listBox2;

th1.Resume;//開始執行

th2.Resume;

end;

end.

由上例可見,當多個線程同時修改一個公用變量時,會産生沖突,所以我們要設法防止它,這樣我們開發的多線程應用才能夠穩定地運行。下面我們來改進它。我們先使用臨界段來串行化,實現同步。在上例unit1.pas代碼的uses段中加入SyncObjs單元,加入全局臨界段變量(TRTLCriticalSection)Critical1,在FormCreate事件中加入InitializeCriticalSection(Critical1)這句代碼,在FormDestroy事件中加入DeleteCriticalSection(Critical1)這句代碼,然後修改TListThread.Execute函數,修改後的代碼似如下所示(?處爲增加的代碼):

procedure TListThread.Execute;

var

I,J,K:Integer;

begin

for i:=0 to 50 do

begin

?EnterCriticalSection(Critical1);//進入臨界段

for J:=1 to 20 do

for K:=1 to 3000 do

if letters[j]<'Z' then

letters[j]:=succ(Letters[j])

else

letters[j]:='A';

str:=letters;

?LeaveCriticalSection(Critical1);//退出臨界段

synchronize(addtolist);

end;

end;

好了,重新編譯,運行結果如下圖所示(略)

程序成功的避免了沖突,看來真的很簡單,我們成功了!當然我們還可以使用其它同步技術如Mutex(互斥對象), Semaphore(信號量)等,這些技術都是Windows通過API直接提供給我們的。

下面總結一下Windows常用的幾種線程同步技術。

1. Critical Sections(臨界段),源代碼中如果有不能由兩個或兩個以上線程同時執行的部分,可以用臨界段來使這部分的代碼執行串行化。它只能在一個獨立的進程或一個獨立的應用程序中使用。使用方法如下:

//在窗體創建中

InitializeCriticalSection(Critical1)

//在窗體銷毀中

DeleteCriticalSection(Critical1)

//在線程中

EnterCriticalSection(Critical1)

……保護的代碼

LeaveCriticalSection(Critical1)

2. Mutex(互斥對象),是用于串行化訪問資源的全局對象。我們首先設置互斥對象,然後訪問資源,最後釋放互斥對象。在設置互斥對象時,如果另一個線程(或進程)試圖設置相同的互斥對象,該線程將會停下來,直到前一個線程(或進程)釋放該互斥對象爲止。注意它可以由不同應用程序共享。使用方法如下:

//在窗體創建中

hMutex:=CreateMutex(nil,false,nil)

//在窗體銷毀中

CloseHandle(hMutex)

//在線程中

WaitForSingleObject(hMutex,INFINITE)

……保護的代碼

ReleaseMutex(hMutex)

3. Semaphore(信號量),它與互斥對象相似,但它可以計數。例如可以允許一個給定資源同時同時被三個線程訪問。其實Mutex就是最大計數爲一的Semaphore。使用方法如下:

//在窗體創建中

hSemaphore:= CreateSemaphore(nil,lInitialCount,lMaximumCount,lpName)

//在窗體銷毀中

CloseHandle(hSemaphore)

//在線程中

WaitForSingleObject(hSemaphore,INFINITE)

……保護的代碼

ReleaseSemaphore(hSemaphore, lReleaseCount, lpPreviousCount)

4. 還可以使用Delphi中的TcriticalSection這個VCL對象,它的定義在Syncobjs.pas中。

當你開發多線程應用時,並且多個線程同時訪問一個共享資源或數據時,你需要考慮線程同步的問題了。

delphi中多線程同步的一些方法

[ 2006-01-09 10:48:03 | 作者: snox 字體大小:大 |中 |小 ]

當有多個線程的時候,經常需要去同步這些線程以訪問同一個數據或資源。例如,假設有一個程序,其中一個線程用于把文件讀到內存,而另一個線程用于統計文件中的字符數。當然,在把整個文件調入內存之前,統計它的計數是沒有意義的。但是,由于每個操作都有自己的線程,操作系統會把兩個線程當作是互不相幹的任務分別執行,這樣就可能在沒有把整個文件裝入內存時統計字數。爲解決此問題,你必須使兩個線程同步工作。

存在一些線程同步地址的問題,Win32提供了許多線程同步的方式。在本節你將看到使用臨界區、 互斥、信號量和事件來解決線程同步的問題。

1. 臨界區

臨界區是一種最直接的線程同步方式。所謂臨界區,就是一次只能由一個線程來執行的一段代碼。如果把初始化數組的代碼放在臨界區內,另一個線程在第一個線程處理完之前是不會被執行的。

在使用臨界區之前,必須使用InitializeCriticalSection()過程來初始化它。

其聲明如下:

procedure InitializeCriticalSection(var

lpCriticalSection參數是一個TRTLCriticalSection類型的記錄,並且是變參。至于TRTLCriticalSection 是如何定義的,這並不重要,因爲很少需要查看這個記錄中的具體內容。只需要在lpCriticalSection中傳遞未初始化的記錄,InitializeCriticalSection()過程就會填充這個記錄。

注意Microsoft故意隱瞞了TRTLCriticalSection的細節。因爲,其內容在不同的硬件平台上是不同的。在基于Intel的平台上,TRTLCriticalSection包含一個計數器、一個指示當前線程句柄的域和一個系統事件的句柄。在Alpha平台上,計數器被替換爲一種Alpha-CPU 數據結構,稱爲spinlock。在記錄被填充後,我們就可以開始創建臨界區了。這時我們需要用EnterCriticalSection()和LeaveCriticalSection()來封裝代碼塊。這兩個過程的聲明如下:

procedure EnterCriticalSection(var lpCriticalSection:TRRLCriticalSection);stdcall;

procedure LeaveCriticalSection(var

正如你所想的,參數lpCriticalSection就是由InitializeCriticalSection()填充的記錄。

當你不需要TRTLCriticalSection記錄時,應當調用DeleteCriticalSection()過程,下面是它的聲明:

procedure DeleteCriticalSection(var

2. 互斥

互斥非常類似于臨界區,除了兩個關鍵的區別:首先,互斥可用于跨進程的線程同步。其次,互斥能被賦予一個字符串名字,並且通過引用此名字創建現有互斥對象的附加句柄。

提示臨界區與事件對象(比如互斥對象)的最大的區別是在性能上。臨界區在沒有線程沖突時,要用1 0 ~ 1 5個時間片,而事件對象由于涉及到系統內核要用400~600個時間片。

可以調用函數CreateMutex ( )來創建一個互斥量。下面是函數的聲明:

function

lpMutexAttributes參數爲一個指向TSecurityAttributtes記錄的指針。此參數通常設爲0,表示默認的安全屬性。bInitalOwner參數表示創建互斥對象的線程是否要成爲此互斥對象的擁有者。當此參數爲False時, 表示互斥對象沒有擁有者。

lpName參數指定互斥對象的名稱。設爲nil表示無命名,如果參數不是設爲nil,函數會搜索是否有同名的互斥對象存在。如果有,函數就會返回同名互斥對象的句柄。否則,就新創建一個互斥對象並返回其句柄。

當使用完互斥對象時,應當調用CloseHandle()來關閉它。

在程序中使用WaitForSingleObject()來防止其他線程進入同步區域的代碼。此函數聲明如下:

function

這個函數可以使當前線程在dwMilliseconds指定的時間內睡眠,直到hHandle參數指定的對象進入發信號狀態爲止。一個互斥對象不再被線程擁有時,它就進入發信號狀態。當一個進程要終止時,它就進入發信號狀態。dwMilliseconds參數可以設爲0,這意味著只檢查hHandle參數指定的對象是否處于發信號狀態,而後立即返回。dwMilliseconds參數設爲INFINITE,表示如果信號不出現將一直等下去。

這個函數的返回值如下

WaitFor SingleObject()函數使用的返回值

返回值 含義

WAIT_ABANDONED 指定的對象是互斥對象,並且擁有這個互斥對象的線程在沒有釋放此對象之前就已終止。此時就稱互斥對象被抛棄。這種情況下,這個互斥對象歸當前線程所有,並把它設爲非發信號狀態

WAIT_OBJECT_0 指定的對象處于發信號狀態

WAIT_TIMEOUT等待的時間已過,對象仍然是非發信號狀態再次聲明,當一個互斥對象不再被一個線程所擁有,它就處于發信號狀態。此時首先調用WaitForSingleObject()函數的線程就成爲該互斥對象的擁有者,此互斥對象設爲不發信號狀態。當線程調用ReleaseMutex()函數並傳遞一個互斥對象的句柄作爲參數時,這種擁有關系就被解除,互斥對象重新進入發信號狀態。

注意除WaitForSingleObject()函數外,你還可以使用WaitForMultipleObject()和MsgWaitForMultipleObject()函數,它們可以等待幾個對象變爲發信號狀態。這兩個函數的詳細情況請看Win32 API聯機文檔。

3. 信號量

另一種使線程同步的技術是使用信號量對象。它是在互斥的基礎上建立的,但信號量增加了資源計數的功能,預定數目的線程允許同時進入要同步的代碼。可以用CreateSemaphore()來創建一個信號量對象,其聲明如下:

function

和CreateMutex()函數一樣,CreateSemaphore()的第一個參數也是一個指向TSecurityAttribute s記錄的指針,此參數的缺省值可以設爲nil。

lInitialCount參數用來指定一個信號量的初始計數值,這個值必須在0和lMaximumCount之間。此參數大于0,就表示信號量處于發信號狀態。當調用WaitForSingleObject()函數(或其他函數)時,此計數值就減1。當調用ReleaseSemaphore()時,此計數值加1。

參數lMaximumCount指定計數值的最大值。如果這個信號量代表某種資源,那麽這個值代表可用資源總數。

參數lpName用于給出信號量對象的名稱,它類似于CreateMutex()函數的lpName參數。

——————————————————————————————————————————

★★★關于線程同步:

Synchronize()是在一個隱蔽的窗口裏運行,如果在這裏你的任務很繁忙,你的主窗口會阻塞掉;Synchronize()只是將該線程的代碼放到主線程中運行,並非線程同步。

臨界區是一個進程裏的所有線程同步的最好辦法,他不是系統級的,只是進程級的,也就是說他可能利用進程內的一些標志來保證該進程內的線程同步,據Richter說是一個記數循環;臨界區只能在同一進程內使用;臨界區只能無限期等待,不過2k增加了TryEnterCriticalSection函數實現0時間等待。

互斥則是保證多進程間的線程同步,他是利用系統內核對象來保證同步的。由于系統內核對象可以是有名字的,因此多個進程間可以利用這個有名字的內核對象保證系統資源的線程安全性。互斥量是Win32 內核對象,由操作系統負責管理;互斥量可以使用WaitForSingleObject實現無限等待,0時間等待和任意時間等待。

1. 臨界區

臨界區是一種最直接的線程同步方式。所謂臨界區,就是一次只能由一個線程來執行的一段代碼。如果把初始化數組的代碼放在臨界區內,另一個線程在第一個線程處理完之前是不會被執行的。在使用臨界區之前,必須使用InitializeCriticalSection()過程來初始化它。

在第一個線程調用了EnterCriticalSection()之後,所有別的線程就不能再進入代碼塊。下一個線程要等第一個線程調用LeaveCriticalSection()後才能被喚醒。

2. 互斥

互斥非常類似于臨界區,除了兩個關鍵的區別:首先,互斥可用于跨進程的線程同步。其次,互斥能被賦予一個字符串名字,並且通過引用此名字創建現有互斥對象的附加句柄。

提示:臨界區與事件對象(比如互斥對象)的最大的區別是在性能上。臨界區在沒有線程沖突時,要用10 ~ 15個時間片,而事件對象由于涉及到系統內核要用400~600個時間片。

當一個互斥對象不再被一個線程所擁有,它就處于發信號狀態。此時首先調用WaitForSingleObject()函數的線程就成爲該互斥對象的擁有者,此互斥對象設爲不發信號狀態。當線程調用ReleaseMutex()函數並傳遞一個互斥對象的句柄作爲參數時,這種擁有關系就被解除,互斥對象重新進入發信號狀態。

可以調用函數CreateMutex()來創建一個互斥量。當使用完互斥對象時,應當調用CloseHandle()來關閉它。

3. 信號量

另一種使線程同步的技術是使用信號量對象。它是在互斥的基礎上建立的,但信號量增加了資源計數的功能,預定數目的線程允許同時進入要同步的代碼。可以用CreateSemaphore()來創建一個信號量對象,

因爲只允許一個線程進入要同步的代碼,所以信號量的最大計數值(lMaximumCount)要設爲1。ReleaseSemaphore()函數將使信號量對象的計數加1;

記住,最後一定要調用CloseHandle()函數來釋放由CreateSemaphore()創建的信號量對象的句柄。

★★★WaitForSingleObject函數的返值:

WAIT_ABANDONED指定的對象是互斥對象,並且擁有這個互斥對象的線程在沒有釋放此對象之前就已終止。此時就稱互斥對象被抛棄。這種情況下,這個互斥對象歸當前線程所有,並把它設爲非發信號狀態;

WAIT_OBJECT_0 指定的對象處于發信號狀態;

WAIT_TIMEOUT等待的時間已過,對象仍然是非發信號狀態;

——————————————————————————————————————————————

VCL支持三種技術來達到這個目的:

(2) 使用critical區

如果對象沒有提高內置的鎖定功能,需要使用critical區,Critical區在同一個時間只也許一個線程進入。爲了使用Critical區,産生一個TCriticalSection全局的實例。TcriticalSection有兩個方法,Acquire(阻止其他線程執行該區域)和Release(取消阻止)

每個Critical區是與你想要保護的全局內存相關聯。每個訪問全局內存的線程必須首先使用Acquire來保證沒有其他線程使用它。完成以後,線程調用Release方法,讓其他線程也可以通過調用Acquire來使用這塊全局內存。

警告:Critical區只有在所有的線程都使用它來訪問全局內存,如果有線程直接調用內存,而不通過Acquire,會造成同時訪問的問題。例如:LockXY是一個全局的Critical區變量。任何一個訪問全局X, Y的變量的線程,在訪問前,都必須使用Acquire

LockXY.Acquire;{ lock out other threads }

try

Y := sin(X);

finally

LockXY.Release;

end

臨界區主要是爲實現線程之間同步的,但是使用的時候注意,一定要在用此臨界對象同步的線程之外建立該對象(一般在主線程中建立臨界對象)。

————————————————————————————————————————————————

線程同步使用臨界區,進程同步使用互斥對象。

Delphi中封裝了臨界對象。對象名爲TCriticalSection,使用的時候只要在主線程當中建立這個臨界對象(注意一定要在需要同步的線程之外建立這個對象)。具體同步的時候使用Lock和Unlock即可。

而進程間同步建立互斥對象,則只需要建立一個互斥對象CreateMutex. 需要同步的時候只需要WaitForSingleObject(mutexhandle, INFINITE) unlock的時候只需要ReleaseMutex(mutexhandle);即可。

有很多方法, 信號燈, 臨界區, 互斥對象,此外, windows下還可以用全局原子,共享內存等等. 在windows體系中, 讀寫一個8位整數時原子的, 你可以依靠這一點完成互斥的方法. 對于能夠産生全局名稱的方法能夠可以在進程間同步上(如互斥對象), 也可以用在線程間同步上;不能夠産生全局名稱的方法(如臨界區)只能用在線程間同步上.

 
(彙)Delphi+MultiThread
關于線程同步的一些方法 線程是進程內一個相對獨立的、可調度的執行單元。一個應用可以有一個主線程,一個主線程可以有多個子線程,子線程還可以有自己的子線程,這樣就構成了多線程應用了。由于多個線程往往會同時...查看完整版>>(彙)Delphi+MultiThread