多執行緒程式設計(轉)

heying1229發表於2007-07-28
多執行緒程式設計:

  本課中,我們將學習如何進行多執行緒程式設計。另外我們還將學習如何在不同的執行緒間進行通訊。
理論:
前一課中,我們學習了程式,其中講到每一個程式至少要有一個主執行緒。這個執行緒其實是程式執行的一條線索,除此主執行緒外您還可以給程式增加其它的執行緒,也即增加其它的執行線索,由此在某種程度上可以看成是給一個應用程式增加了多工功能。當程式執行後,您可以根據各種條件掛起或執行這些執行緒,尤其在多CPU的環境中,這些執行緒是併發執行的。這些是在W32下才有的概念,在WIN16下並沒有等同的概念。
在同一程式中執行不同的執行緒的好處是這些執行緒可以共享程式的資源,如全域性變數、資源等。當然各個執行緒也可以有自己的私有棧用於儲存私有資料。另外每個執行緒需要儲存其執行上下文以便線上程切換時能夠記住或恢復其上下文,當然這是由作業系統來完成的,對於使用者是透明的。
我們大體上可以把執行緒分成兩大類:
處理使用者介面的執行緒:該類執行緒產生自己的視窗並負責處理相關的視窗訊息。使用者介面執行緒遵守WIN16下的互斥原則,即沒一刻僅有一個使用者介面執行緒使用USER和GDI庫中的核心函式,也就是說當一個使用者介面程式在進入GDI或USER中時,核心不允許重入。由此我們可以推論出WIN95的該部分核心的程式碼是遵守16位模式的。而WINOWS NT是純的32位作業系統,所以不存在這個問題。
工作者執行緒:該類執行緒不用處理視窗介面,當然也就不用處理訊息了。它一般都執行在後臺幹一些計算之類的粗,這大概也是把它叫做工作者執行緒的原因吧。
運用W32的多執行緒模式來程式設計,我們可以遵循某種策略:即讓主執行緒僅來做使用者介面的工作,而其它繁重的工作則交由工作者執行緒在後臺完成。這就好比我們日常生活中的許多例子。譬如:政府管理者好比是使用者介面執行緒,它負責聽取民意,給職能部門分配工作,然後把工作成果彙報給公眾。而具體的職能部門就是工作者執行緒,它負責完成下達的具體工作。如果讓政府管理這來具體地做每一件事,它必須作一件事後再做另一項,那它就不能及時來聽取和反饋民意。這樣就無法管理好一個國家了。當然即使採用多執行緒制,政府管理部門也不一定就能管理好國家,但是程式卻可以採用多執行緒機制來管理好她自己的工作。我們可以呼叫CreateThread函式來生成新執行緒。該函式的語法如下:

CreateThread proto lpThreadAttributes:DWORD,
dwStackSize:DWORD,
lpStartAddress:DWORD,
lpParameter:DWORD,
dwCreationFlags:DWORD,
lpThreadId:DWORD

生成一個執行緒的函式和生成一個程式基本相同。
lpThreadAttributes --&gt如果您想要執行緒有預設的屬性,可以置該值為NULL。
dwStackSize --&gt 指定執行緒的堆疊大小。如果為0,那執行緒的大小和程式相同。
lpStartAddress--&gt 執行緒函式的起始地址。注意該函式僅接收一個32位的引數和返回一個32位的值。(該引數可以是一個指標,而且程式的執行緒可以直接存取程式定義全域性變數,所以您大可不必擔心不能如何把大量的引數傳遞給執行緒)。
lpParameter --&gt 傳遞給執行緒的上下文。
dwCreationFlags --&gt如果是0的話則表示創執行緒建後立即啟動,相反的是標誌位CREATE_SUSPENDED,這樣您需要稍後顯示地讓該執行緒執行。
lpThreadId --&gt 核心給新生成的執行緒分配的執行緒ID。

如果生成執行緒成功的話,CreateThread函式就返回新執行緒的控制程式碼。否則返回NULL。
如果沒有給引數dwCreationFlags指定CREATE_SUSPENDED的話,該執行緒就會立即執行。如果不這樣,我們上面說了,需要顯示地啟動該執行緒,要這樣做您需要呼叫ResumeThread函式。
線上程返回後(執行緒的執行類似與執行一個函式,如果它呼叫了最後一條指令後,在彙編中是ret,那麼該執行緒就結束了,除非您讓它進入一個迴圈,譬如我們講的使用者介面執行緒就是如此,只不過它不退出的原因是進入的迴圈是在{while ( GetMessage(...))...}中,如果您沒有給它傳遞一個值為0的訊息,那它可不會退出),系統會自動呼叫ExitThread函式透明地處理執行緒一些退出時的清理工作。當然您可以自己呼叫該函式,但似乎沒有什麼意義。要得到退出時的退出碼,您可以呼叫GetExitCodeThread函式。
如果您想結束一個程式,可以呼叫TerminateThread函式,不過使用該函式要小心行事,因為該函式一旦被呼叫執行緒就會退出,這樣它就沒有機會來做清理自己的工作了。

現在我們來看看執行緒間的通訊機制。
總的說來一共有三種方法:

使用全域性變數
使用Windows訊息傳遞機制
使用事件
上面我們說了執行緒會共享程式的資源,其中全域性變數也包括在內,所以執行緒可以透過使用全域性變數來通訊。但是這種辦法的明顯的缺點是在有多個執行緒存取同一個全域性變數時,必須考慮同步的問題。譬如:有一個有十個成員變數的結構體,其中一個執行緒在對起賦值時,假設只更新了五個成員變數的值,這時核心的排程執行緒剝奪其執行權給另一個執行緒,這樣接下來的執行緒如果想要用該全域性結構體變數,它的值就顯然不對了。另外多執行緒的程式也很難除錯,尤其這些錯誤很隱蔽和很難復現時。如果兩個執行緒都是使用者介面執行緒時,用WINDOWS的訊息機制來進行執行緒間的通訊是比較方便的.
您所要做的只是自定義一些windows訊息(注意不要和windows的預定義的訊息衝突),然後線上程之間傳遞可以了。您可以這樣來定義訊息,把WM_USER(它的值等於0x0400)當作基數,然後順序地去加序號,譬如:
WM_MYCUSTOMMSG equ WM_USER+100h

小於WM_USER 的值是Windows系統的保留值,大於該值留給使用者來使用。
如果其中有一個執行緒是工作者執行緒的話,那就不能用該種方法來進行通訊了,這是因為工作者執行緒沒有訊息佇列。您應當用下面這種策略來進行工作者執行緒和使用者介面執行緒之間的通訊:

User interface Thread ------&gt global variable(s)----&gt Worker thread
Worker Thread ------&gt custom window message(s) ----&gt User interface Thread

稍後我們的例子中將講解這種通訊辦法。
最後的辦法是事件物件。您可以把事件物件看作是一種標誌。如果事件物件的狀態是無訊號的話,說明該執行緒正在睡眠或掛起,在該種狀態下系統是不會給該執行緒分配CPU時間片的。當一個執行緒的狀態轉成有訊號時,WINDOWS就會喚醒該執行緒並且讓它正常執行。

例子:
您可以下載例子並執行thread1.exe,然後啟用選單項"Savage Calculation",然後程式開始執行指令"add eax,eax ",一共執行600,000,000次,您會發現在這個過程當中,使用者介面將停止響應,您既不能使用選單,也不能使用移動視窗。等到計算完成後,會彈出一個對話方塊,關閉掉對話方塊後視窗才可以和當初一樣正常執行了。
為了避免這種不便,我們把計算的工作放入到一個單獨的工作者執行緒中去,而主視窗僅僅響應使用者的活動。您可以看到雖然使用者介面的反應比平常時慢了,但還是可以工作的。
.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include masm32includewindows.inc
include masm32includeuser32.inc
include masm32includekernel32.inc
includelib masm32libuser32.lib
includelib masm32libkernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,
CW_USEDEFAULT,300,200,NULL,NULL,
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,
0,
ADDR ThreadID
invoke CloseHandle,eax
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

end start

分析:
主程式的主執行緒是一個使用者介面執行緒,它有一個普通視窗。使用者選擇選單項"Create Thread",程式就會產生一個執行緒:
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,
NULL,0,
ADDR ThreadID
invoke CloseHandle,eax

上面的程式碼段產生一個執行緒,執行緒的主體程式碼是函式ThreadProc,該函式和主執行緒並行執行。在呼叫成功後,CreateThread函式立即返回,ThreadProc也開始執行。因為我們不再用執行緒控制程式碼,我們立即關閉它以避免記憶體洩漏。我們前面講過關閉控制程式碼不會終止執行緒的執行,而只是減少起引用計數。

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

我們看到上面的執行緒的程式碼僅僅是做簡單的計數工作,因為我們設了一個很大的基數,所以該執行緒會持續一段您能感覺得到的時間,當結束後它會向主執行緒傳送WM_FINISH訊息。WM_FINISH訊息是我們自己定義的,它的定義如下:

WM_FINISH equ WM_USER+100h
WM_USER訊息是我們能夠使用的最小訊息值。
顯然我們一看到WM_FINISH,就能從字面上理解該訊息的意義。主執行緒接收到該訊息後,會彈出一個對話方塊告訴使用者,計算執行緒已經結束了。
透過執行緒之間的通訊,使用者可以多次選擇"Create Thread",那樣就可以執行多個計算執行緒了。
本例子中,執行緒之間的通訊是單向的。如果您想讓主執行緒也能向工作者執行緒傳送訊息的話,譬如加入一個選單項來控制工作者執行緒的結束,您可以這樣做:
add a menu item saying something like "Kill Thread" in the menu
a global variable which is used as a command flag. TRUE=Stop the thread, FALSE=continue the thread
Modify ThreadProc to check the value of the command flag in the loop.
設立一個全域性變數,當執行緒啟動前,我們設定它的值為FALSE,當使用者啟用了我們加的選單項時,該值變成TRUE。線上程的程式碼段ThreadProc中每次減1前,判斷該值,如果為TRUE的話執行緒就結束迴圈體中的計算並退出執行緒。

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10172717/viewspace-928729/,如需轉載,請註明出處,否則將追究法律責任。

相關文章