WIN 98/NT/2000/XP是個多任務操作系統(tǒng),也就是:一個進程可以劃分為多個線程,每個線程輪流占用CPU運行時間和資源,或者說,把CPU 時間劃成片,每個片分給不同的線程,這樣,每個線程輪流的“掛起”和“喚醒”,由于時間片很小,給人的感覺是同時運行的。
多線程帶來如下好處:
1)避免瓶頸;
2)并行操作;
3)提高效率;
多線程的兩個概念:
1) 進程:也稱任務,程序載入內(nèi)存,并分配資源,稱為“一個進程”。
注意:進程本身并不一定要正在執(zhí)行。進程由以下幾部分組成:
a>一個私有的地址空間,它是進程可以使用的一組虛擬內(nèi)存地址空間;
b>程序的相關代碼、數(shù)據(jù)源;
c>系統(tǒng)資源,比如操作系統(tǒng)同步對象等;
d>至少包含一個線程(主線程);
2) 線程:是進程的執(zhí)行單位(線程本身并不包括程序代碼,真正擁有代碼的是進程),是操作系統(tǒng)分配CPU時間的基本實體,每個進程至少包括一個線程,稱為主線程。一個進程如果有多個線程,就可以共享同一進程的資源,并可以并發(fā)執(zhí)行。通俗點說就是進程中一段并發(fā)運行的代碼(一個函數(shù)或過程)。
線程主要由如下兩部分組成:
a>數(shù)據(jù)結構;
b>CPU 寄存器和堆棧;
線程函數(shù)運行,啟動函數(shù)就返回了,主線程繼續(xù)向下執(zhí)行,而線程函數(shù)在一個獨立的線程中執(zhí)行,它要執(zhí)行多久,什么時候返回,主線程是不管也不知道的。
一、Delphi線程對象--- Tthread
雖然Windows提供了較多的多線程設計的API 函數(shù),但是直接使用API 函數(shù)極其不方便,而且使用不當還容易出錯。為解決這個問題,Borland公司率先推出了一種Tthread 對象,來解決多線程設計上的困難,簡化了多線程問題的處理。
一、Tthread對象的主要方法
構造線程:
constructor Create(CreateSuspended:boolean)
CreateSuspended=true構造但不喚醒 ;false構造的同時即喚醒 。
掛起線程:
suspend:(把線程掛起的次數(shù)加一)
喚醒線程:
resume :(注意:注意這個屬性是把線程掛起的次數(shù)減一,當次數(shù)為0時,即喚醒。也就是說,線程掛起多少次,喚醒也需要多少次。同時掛起的時候?qū)⒈3志程的地址指針不變,所以線程掛起后再喚醒,將從掛起的地方開始運行)
析構(清除線程所占用的內(nèi)存):
destroy
終止線程
Terminate
使用這個類也很簡單,基本用法是:先從TThread派生一個自己的線程類(因為TThread
是一個抽象類,不能生成實例),然后是覆蓋(Override)抽象方法:Execute(這就是線程函數(shù),也就是在線程中執(zhí)行的代碼部分),如果需要用到可視VCL對象,還需要通過Synchronize過程進行。
線程的終止和退出:
1)自動退出:
一個線程從Execute()過程中退出,即意味著線程的終止,此時將調(diào)用Windows的ExitThread()函數(shù)來清除線程所占用的堆棧。
如果線程對象的 FreeOnTerminate屬性設為True,則線程對象將自動刪除,并釋放線程所占用的資源。
這是消除線程對象最簡單的辦法。
2)受控退出:
利用線程對象的Terminate屬性,可以由進程或者由其他線程控制線程的退出。只需要簡單的調(diào)用該線程的Terminate方法,并設置線程對象的Terminate屬性為True。
一般來說,在線程中,應該不斷監(jiān)視Terminate的值,一旦發(fā)現(xiàn)為True,則退出,一般來說,例如在Execute()過程中可以這樣寫:
While not Terminate do
begin
........
end;
3)退出的API函數(shù):
關于線程退出的API函數(shù)聲明如下:
Function TerminateThread(hThread:Thandle;dwExitCode:DWORD);
不過,這個函數(shù)會使代碼立刻終止,而不管程序中有沒有
try....finally
機制,可能會導致錯誤,不到萬不得已,最好不要使用。
4)利用掛起線程的方法(suspend)
利用掛起線程的suspend方法,后面跟個Free,也可以釋放線程,
例如:
thread1.suspend; //掛起
thread2.free; //釋放
二、多線程的同步機制
同步機制,研究多線程的同步機制的必要性在于,多線程同步工作時,如果同時調(diào)用相同的資源,就可能會出現(xiàn)問題,如對全局變量、數(shù)據(jù)庫操作發(fā)生沖突,甚至產(chǎn)生死鎖和競爭問題。
舉個發(fā)生沖突的實例看一下:
一般來說,對內(nèi)存數(shù)據(jù)加一的操作分解以后有三個步驟:
1、從內(nèi)存中讀出數(shù)據(jù)
2、數(shù)據(jù)加一
3、存入內(nèi)存
現(xiàn)在假設在一個兩個線程的應用中用Inc進行加一操作可能出現(xiàn)的一種情況:
1、線程A從內(nèi)存中讀出數(shù)據(jù)(假設為3)
2、線程B從內(nèi)存中讀出數(shù)據(jù)(也是3)
3、線程A對數(shù)據(jù)加一(現(xiàn)在是4)
4、線程B對數(shù)據(jù)加一(現(xiàn)在也是4)
5、線程A將數(shù)據(jù)存入內(nèi)存(現(xiàn)在內(nèi)存中的數(shù)據(jù)是4)
6、線程B也將數(shù)據(jù)存入內(nèi)存(現(xiàn)在內(nèi)存中的數(shù)據(jù)還是4,但兩個線程都對它加了一,應該是5才對,所以這里出現(xiàn)了錯誤的結果)
1.臨界區(qū)(Critical Sections)
臨界區(qū)(CriticalSection)是一項共享數(shù)據(jù)訪問保護的技術。對它只有兩個操作:Enter和Leave,這兩個操作也是原子操作。
它的保護原理是這樣的:當一個線程A調(diào)用某一個Enter后,開始訪問某個數(shù)據(jù)D,如果此時另一個線程B也要訪問數(shù)據(jù)D,則它會在調(diào)用這個Enter時,發(fā)現(xiàn)已經(jīng)有線程進入臨界區(qū),然后線程B就會被掛起,等待線程A調(diào)用Leave。當線程A完成操作,調(diào)用Leave離開后,線程B就會被喚醒,并設置臨界區(qū)標志,開始操作數(shù)據(jù),這樣就防止了訪問沖突
var
CS:TRTLCriticalSection;//被聲明在程序最上方,作為線程都可以使用的全局變量。
initializeCriticalSection(cs); //初始化
Procedure InterlockedIncrement( var aValue : Integer );
Begin
EnterCriticalSection(CS);//獨占
Inc( aValue );
LeaveCriticalSection(CS); //解除獨占
End;
現(xiàn)在再來看前面那個例子:
1. 線程A進入臨界區(qū)(假設數(shù)據(jù)為3)
2. 線程B進入臨界區(qū),因為A已經(jīng)在臨界區(qū)中,所以B被掛起
3. 線程A對數(shù)據(jù)加一(現(xiàn)在是4)
4. 線程A離開臨界區(qū),喚醒線程B(現(xiàn)在內(nèi)存中的數(shù)據(jù)是4)
5. 線程B被喚醒,對數(shù)據(jù)加一(現(xiàn)在就是5了)
6. 線程B離開臨界區(qū),現(xiàn)在的數(shù)據(jù)就是正確的了。
臨界區(qū)就是這樣保護共享數(shù)據(jù)的訪問
請注意,臨界區(qū)只能在一個進程內(nèi)使用,可以在多處設置調(diào)用enter。
不要長時間鎖住一份資源,如果你一直讓資源被鎖定,你就會阻止其它線程的執(zhí)行,并把整個程序帶到一個完全停止的狀態(tài),所以千萬不要在一個ciritical section中調(diào)用sleep()或任何Wait…()函數(shù)。
ciritical section的一個缺點是,它不是核心對象,如果進入ciritical section的那個線程結束了或者當?shù)袅耍鴽]有調(diào)用LeaveCriticalSection的話,系統(tǒng)沒有辦法將該ciritical section清除,如果你需要這樣的機能,你應該使用mutex。
VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection//一個指針,指向欲被初始化的CRITICAL_SECTION變量
);
函數(shù)功能:初始化一個臨界對象,當你用畢臨界對象時,必須調(diào)用DeleteCriticalSection()清除它。
VOID DeleteCriticalSection (
LPCRITICAL_SECTION lpCriticalSection// 臨界對象指針
);
函數(shù)功能:申請刪除臨界對象
VOID EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection//臨界對象指針
);
函數(shù)功能:申請進入臨界對象
VOID LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection//臨界對象指針
);
函數(shù)功能
申請進入臨界對象
2.互斥器(Mutexes)
一個時間內(nèi)只能夠有一個線程擁有mutex,就好像同一個時間只能夠有一個線程進入同一臨界區(qū)。
Mutex和critical section還是有差別的:
1.鎖住一個未被使用的Mutexes,比鎖住一個未被使用的critical section,需要花費幾乎100倍的時間
2. Mutexes可以跨進程使用,critical section則只能夠在同一個進程中使用
3.等待一個Mutexes時,你可以指定結束等待的時間長度,但對于critical section則不行。
兩種對象的相關函數(shù)比較:
CRITICAL_SECTION | Mutex核心對象 |
InitializeCriticalSection() | CreateMutex() |
OpenMutex() | |
EnterCriticalSection() | WaitForSingleObject() |
WaitForMultipleObject() | |
MsgWaitForMutipleObjects() | |
LeaveCriticalSection() | ReleaseMutex() |
DeleteCriticalSection() | CloseHandle() |
Mutex的使用機制:
1. 有一個mutex,此時沒有任何線程擁有它,此時它處于非激發(fā)狀態(tài)。
2. 某個線程調(diào)用WaitforSingleObject()或任何其它的wait…函數(shù),并指定該mutex的handle為參數(shù)
3. win32于是將該mutex的擁有權給予這個線程,然后將此mutex設為激發(fā)狀態(tài),于是wait..函數(shù)返回
4. wait..函數(shù)返回后,win32立刻又將mutex設為非激發(fā)狀態(tài),是任何處于等待狀態(tài)下的其它線程沒有辦法獲得其擁有權
5. 獲得該mutex的線程調(diào)用Release,將mutex釋放掉。于是循環(huán)到第一步。
如果線程擁有一個mutex,而在結束前沒有調(diào)用releaseMutex,mutex不會被摧毀,該mutex會被win32視為“未被擁有”以及“未被激發(fā)”,下一個等待中的線程會被以WAIT_ABANDONED_0通知。如果是WaitForMultipleObjects()等待辭mutex,函數(shù)返回值介于WAIT_ABANDONED_0和WAIT_ABANDONED_0+n之間,n是指handle數(shù)組的元素個數(shù)。
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
參數(shù)
lpMutexAttributes:安全屬性。Null表示使用默認的屬性。
bInitialOwner:如果你希望調(diào)用這個函數(shù)的線程擁有mutex,就將此值設為true
lpName:互斥對象的名稱
返回值
如果成功,則返回一個handle,否則返回null。
函數(shù)說明:
如果指定的mutex名稱已經(jīng)存在,win32會給你一個mutex handle,而不會為你產(chǎn)生一個新的mutex。調(diào)用GetLastError會傳回ERROR_ALREADY_EXISTS。當你不需要mutex時,你可以調(diào)用closehandle()將它關閉。
BOOL ReleaseMutex(
HANDLE hMutex //欲釋放mutex的handle
);
返回值
如果成功,則返回true,否則返回false。
3.信號量(Semaphores)
Mutex是semaphore的一種退化,如果你產(chǎn)生一個semaphore并令最大值為1,那它就是個mutex。因此,mutex又常被稱為binary semaphore。在許多系統(tǒng)中,semaphore常被使用,因為mutex可能并不存在,在win32中semaphore被使用的情況就少得多,因為mutex存在的原因。
一旦semaphore的現(xiàn)值降到0,就表示資源已經(jīng)耗盡。此時任何線程如果調(diào)用Wait…函數(shù),必然要等待,直到某個鎖定被解除。
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
)
參數(shù):
lpSemaphoreAttributes:安全屬性,null表示使用默認屬性。
lInitialCount:初始值,必須>=0,并且<= lMaximumCount
lMaximumCount:最大值,也就是在同一時間內(nèi)能夠鎖住semaphore的線程數(shù)
lpName:名稱,這個值也可以是null。
返回值:
如果成功就返回一個handle,否則返回null。如果semaphore名稱已經(jīng)存在,函數(shù)還是會成功,GetLastError會返回ERROR_ALREADY_EXISTS
函數(shù)說明:
產(chǎn)生一個semaphore。
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
參數(shù):
hSemaphore:semaphore的句柄。
lReleaseCount:semaphore現(xiàn)值的增量,通常是1,該值不可以是負值或者0
lpPreviousCount:返回semaphore增加前的現(xiàn)值
返回值:
如果成功就返回true,否則返回false。
函數(shù)說明:
三、事件(Events)
事件(Event)是一種核心對象,它的唯一目的就是成為激發(fā)狀態(tài)或未激發(fā)狀態(tài)。這兩種狀態(tài)完全在你的掌握之下,不會因為Wait…函數(shù)的調(diào)用而變化。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
參數(shù):
lpEventAttributes:安全屬性,null表示使用默認屬性。
bManualReset :
此值為false,表示event變成激發(fā)狀態(tài)后,自動重置為非激發(fā)狀態(tài);
此值為true,表示不會自動重置,必須靠程序(調(diào)用ResetEvent)操作才能將激發(fā)狀態(tài)的event重置為非激發(fā)狀態(tài)。
bInitialState :初始狀態(tài),true一開始處于激發(fā)狀態(tài),false一開始處于非激發(fā)狀態(tài)
lpName :Event對象名
返回值:
如果成功就返回一個handle,否則返回null。如果event名稱已經(jīng)存在,函數(shù)還是會成功,GetLastError會返回ERROR_ALREADY_EXISTS
BOOL SetEvent(HANDLE hEvent);
//把event對象設為激發(fā)狀態(tài)
BOOL ResetEvent(HANDLE hEvent);
//把event對象設為非激發(fā)狀態(tài)
BOOL PulseEvent(HANDLE hEvent );
//如果event的bManualReset 為true:把event對象設為激發(fā)狀態(tài),喚醒所有等待中的線程,然后event恢復為非激發(fā)狀態(tài)
//如果event的bManualReset 為false:把event對象設為激發(fā)狀態(tài),喚醒一個等待中的線程,然后event恢復為非激發(fā)狀態(tài)
5.使用Synchronize方法
這個方法用于訪問VCL主線程所管理的資源,其方法的應用是:
第一步:把訪問主窗口(或主窗口控件資源)的代碼放到線程的一個方法中;
第二步:是在線程對象的Execute方法中,通過Synchronize方法使用該方法。
實例:
procedure Theater.Execute;
begin
Synchronize(update);
end;
procedure Theater.update;
begin
.........
end;
這里通過 Synchronize使線程方法update同步。
6、使用VCL類的Look方法
在Delphi的IDE提供的構件中,有一些對象內(nèi)部提供了線程的同步機制,工作線程可以直接使用這些控件,比如:Tfont,Tpen,TBitmap,TMetafile,Ticon等。另外,一個很重要的控件對象叫TCanvas,提供了一個Lock方法用于線程的同步,當一個線程使用此控件對象的時候,首先調(diào)用這個對象的Lock方法,然后對這個控件進行操作,完畢后再調(diào)用Unlock方法,釋放對控間的控制權。
例如:
CanversObject.look;
try
畫圖
finally
CanversObject.unlock;
end;
{使用這個保護機制,保證不論有沒有異常,unlock都會被執(zhí)行否則很可能會發(fā)生死鎖。在多線程設計的時候,應該很注意發(fā)生死鎖的問題}
四、線程的優(yōu)先級:
在多線程的情況下,一般要根據(jù)線程執(zhí)行任務的重要性,給線程適當?shù)膬?yōu)先級,一般如果量的線程同時申請CPU時間,優(yōu)先級高的線程優(yōu)先。
優(yōu)先權類別(Priority Class)
Win32提供四種優(yōu)先權類別,每一個類別對應一個基本的優(yōu)先權層次。
表格5-1優(yōu)先權類別
優(yōu)先權類別 | 基礎優(yōu)先權值 |
HIGH_PRIORITY_CLASS | 13 |
IDLE_PRIORITY_CLASS | 4 |
NORMAL_PRIORITY_CLASS | 7or8 |
REALTIME_PRIORITY_CLASS | 24 |
大部分程序使用NORMAL_PRIORITY_CLASS。優(yōu)先權類別是進程的屬性之一,利用SetPriorityClass和GetPriorityClass函數(shù)可以調(diào)整和獲取該值。
優(yōu)先權層次(priority Level)
線程的優(yōu)先權層次使你能夠調(diào)整同一個進程內(nèi)的各線程的相對重要性。一共七種優(yōu)先權層次:
表格5-2
優(yōu)先權層次 | 調(diào)整值 |
THREAD_PRIORITY_LOWEST | -2 |
THREAD_PRIORITY_BELOW_NORMAL | -1 |
THREAD_PRIORITY_NORMAL | 0 |
THREAD_PRIORITY_ABOVE_NORMAL | +1 |
THREAD_PEIOEITY_HIGHEST | +2 |
THREAD_PRIORITY_IDLE | 1 |
THREAD_PRIORITY_TIME_CRITICAL | 15 |
利用SetThreadPriority和GetThreadPriority函數(shù)可以調(diào)整和獲取該值
在Windows下,給線程的優(yōu)先級分為30級,而Delphi中Tthread對象相對簡單的把優(yōu)先級分為七級。也就是在Tthread中聲明了一個枚舉類型TTthreadPriority:
type
TTthreadPriority(tpidle,tpLowest,tpLower,tpNormal,
tpHight,tpHighest,tpTimecrital)
分別對應的是最低(系統(tǒng)空閑時有效,-15),較低(-2),低(-1),正常(普通0),高(1),較高(2),最高(15)。
設置優(yōu)先級可使用thread對象的priority屬性:
threadObject.priority:=Tthreadpriority(級別);
BOOL SetThreadPriority(
HANDLE hThread, //欲調(diào)整優(yōu)先權的那個線程的句柄
int nPriority //表格5-2所顯示的值
);
Int GetThreadPriority(
HANDLE hThread //線程的句柄
);
返回值是線程的優(yōu)先級。