本課中,我們將學習如何進行多線程編程。另外我們還將學習如何在不同的線程間進行通信。
理論:
前一課中,我們學習了進程,其中講到每一個進程至少要有一個主線程。這個線程其實是進程執行的一條線索,除此主線程外您還可以給進程增加其它的線程,也即增加其它的執行線索,由此在某種程度上可以看成是給一個應用程序增加了多任務功能。當程序運行後,您可以根據各種條件掛起或運行這些線程,尤其在多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 -->如果您想要線程有缺省的安全屬性,可以置該值為NULL。
dwStackSize --> 指定線程的堆棧大小。如果為0,那線程的大小和進程相同。
lpStartAddress--> 線程函數的起始地址。注意該函數僅接收一個32位的參數和返回一個32位的值。(該參數可以是一個指針,而且進程的線程可以直接存取進程定義全局變量,所以您大可不必擔心不能如何把大量的參數傳遞給線程)。
lpParameter --> 傳遞給線程的上下文。
dwCreationFlags -->如果是0的話則表示創線程建後立即啟動,相反的是標志位CREATE_SUSPENDED,這樣您需要稍後顯示地讓該線程運行。
lpThreadId --> 內核給新生成的線程分配的線程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 ------> global variable(s)----> Worker thread
Worker Thread ------> custom window message(s) ----> 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 \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.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的話線程就結束循環體中的計算並退出線程。