20. 多工和多執行緒

Post date: 2012/3/23 上午 05:42:31

20. 多工和多執行緒

多工是一個作業系統可以同時執行多個程式的能力。基本上,作業系統使用一個硬體時鐘為同時執行的每個程序配置「時間片段」。如果時間片段夠小,並且機器也沒有由於太多的程式而超出負荷時,那麼在使用者看來,所有的這些程式似乎在同時執行著。

多工並不是什麼新的東西。在大型電腦上,多工是必然的。這些大型主機通常有幾十甚至幾百個終端機和它連結,而每個終端機使用者都應該感覺到他或者她獨佔了整個電腦。另外,大型主機的作業系統通常允許使用者「提交工作到背景」,這些背景作業可以在使用者進行其他工作時,由機器執行完成。

個人電腦上的多工花了更長的時間才普及化。但是現在PC多工也被認為是很正常的了。我馬上就會討論到,Microsoft Windows的16位元版本支援有限度的多工,Windows的32位元版本支援真正的多工,而且,還多了一種額外的優點,多執行緒。

多執行緒是在一個程式內部實作多工的能力。程式可以把它自己分隔為各自獨立的「執行緒」,這些執行緒似乎也同時在執行著。這一概念初看起來似乎沒有什麼用處,但是它可以讓程式使用多執行緒在背景執行冗長作業,從而讓使用者不必長時間地無法使用其電腦進行其他工作(有時這也許不是人們所希望的,不過這種時候去沖沖涼或者到冰箱去看看總是很不錯的)!但是,即使在電腦繁忙的時候,使用者也應該能夠使用它。

多工的各種模式

在PC的早期,有人曾經提倡未來應該朝多工的方向前進,但是大多數的人還是很迷惑:在一個單使用者的個人電腦上,多工有什麼用呢?好了,最後事實表示即使是不知道這一概念的使用者也都需要多工的。

DOS下的多工

在最初PC上的Intel 8088微處理器並不是為多工而設計的。部分原因(我在 上一章 中討論過)是記憶體管理不夠強。當啟動和結束多個程式時,多工的作業系統通常需要移動記憶體塊以收集空閒記憶體。在8088上是不可能透明於應用系統來做到這一點的。

DOS本身對多工沒有太大的幫助,它的設計目的是盡可能小巧,並且與獨立於應用程式之外,因此,除了載入程式以及對程式提供檔案系統的存取功能,它幾乎沒有提供任何支援。

不過,有創意的程式寫作者仍然在DOS的早期就找到了一種克服這些缺陷的方法,大多數是使用常駐(TSR:terminate-and-stay-resident)程式。有些TSR,比如背景列印佇列程式等,透過攔截硬體時鐘中斷來執行真正的背景處理。其他的TSR,諸如SideKick等突現式工具,可以執行某種型態的工作切換-暫停目前的應用程式,執行突現式工具。DOS也逐漸有所增強以便提供對TSR的支援。

一些軟體廠商試圖在DOS之上架構出工作切換或者多工的外殼程式(shell)(諸如Quarterdeck的DesqView),但是在這些環境中,僅有其中一個佔據了大部分市場,當然,這就是Windows。

非優先權式的多工

當Microsoft在1985年發表Windows 1.0時,它是最成熟的解決方案,目的是突破DOS的侷限。Windows在實際模式下執行。但是即使這樣,它已可以在實體記憶體中移動記憶體塊。這是多工的前提,雖然移動的方法尚未完全透明於應用程式,但是幾乎可以忍受了。

在圖形視窗環境中,多工比在一種命令列單使用者作業系統中顯得更有意義。例如,在傳統的命令列UNIX中,可以在命令列之外執行程式,讓它們在背景執行。然而,程式的所有顯示輸出必須被重新轉向到一個檔案中,否則輸出將和使用者正在做的事情混在一起。

視窗環境允許多個程式在相同螢幕上一起執行,前後切換非常容易,並且還可以快速地將資料從一個程式移動到另一個程式中。例如,將繪圖程式中建立的圖片嵌入由文書處理程式編輯的文字檔案中。在Windows中,以多種方式支援資料轉移,首先是使用剪貼簿,後來又使用動態資料交換(DDE),而現在則是透過物件連結和嵌入(OLE)。

不過,早期Windows的多工實作還不是多使用者作業系統中傳統的優先權式的分時多工。這些作業系統使用系統時鐘周期性地中斷一個工作並開始另一個工作。Windows的這些16位元版本支援一種被稱為「非優先權式的多工」,由於Windows訊息驅動的架構而使這種型態的多工成為可能。通常情況下,一個Windows程式將在記憶體中睡眠,直到它收到一個訊息為止。這些訊息通常是使用者的鍵盤或滑鼠輸入的直接或間接結果。當處理完訊息之後,程式將控制權返回給Windows。

Windows的16位元版本不會絕對地依據一個timer tick將控制權從一個Windows程式切換到另一個,任何的工作切換都發生在當程式完成對訊息的處理後將控制權返回給Windows時。這種非優先權式的多工也被稱為「合作式的多工」,因為它要求來自應用程式方面的一些合作。一個Windows程式可以佔用整個系統,如果它要花很長一段時間來處理訊息的話。

雖然非優先權式的多工是16位元Windows的一般規則,但仍然出現了某些形式的優先權式多工。Windows使用優先權式多工來執行DOS程式,而且,為了實作多媒體,還允許動態連結程式庫接收硬體時鐘中斷。

16位元Windows包括幾個功能特性來幫助程式寫作者解決(或者,至少可以說是對付)非優先權式多工中的侷限,最顯著的當然是時鐘式滑鼠游標。當然,這並非一種解決方案,而僅僅是讓使用者知道一個程式正在忙於處理一件冗長作業,因而讓使用者在一段時間內無法使用系統。另一種解決方案是Windows計時器,它允許程式周期性地接收訊息並完成一些工作。計時器通常用於時鐘應用和動畫。

針對非優先權式多工的另一種解決方案是PeekMessage函式呼叫,我們曾在 第五章中的RANDRECT程式 裏看到過。一個程式通常使用GetMessage呼叫從它的訊息佇列中找尋下一個訊息,不過,如果在訊息佇列中沒有訊息,那麼GetMessage不會傳回,一直到出現一個訊息為止。而另一方面,PeekMessage將控制權傳回程式,即使沒有等待的訊息。這樣,一個程式可以執行一個冗長作業,並在程式碼中混入PeekMessage呼叫。只要沒有這個程式或其他任何程式的訊息要處理,那麼這個冗長作業將繼續執行。

Presentation Manager和序列化的訊息佇列

Microsoft在一種半DOS/半Windows的環境下實作多工的第一個嘗試(和IBM合作)是OS/2和Presentation Manager(縮寫成PM )。雖然OS/2明確地支援優先權式多工,但是這種多工方式似乎並未在Presentation Manager中得以落實。問題在於PM序列化來自鍵盤和滑鼠的使用者輸入訊息。這意味著,在前一個使用者輸入訊息被完全處理以前,PM不會將一個鍵盤或者滑鼠訊息傳送給程式。

儘管鍵盤和滑鼠訊息只是一個PM(或者Windows)程式可以接收的許多訊息中的幾個,大多數的其他訊息都是鍵盤或者滑鼠事件的結果。例如,功能表命令訊息是使用者使用鍵盤或者滑鼠進行功能表選擇的結果。在處理功能表命令訊息時,鍵盤或者滑鼠訊息並未完全被處理。

序列化訊息佇列的主要原因是允許使用者的預先「鍵入」鍵盤按鍵和預先「按入」滑鼠按鈕。如果一個鍵盤或者滑鼠訊息導致輸入焦點從一個視窗切換到另一個視窗,那麼接下來的鍵盤訊息應該進入擁有新的輸入焦點的視窗中去。因此,系統不知道將下一個使用者輸入訊息發送到何處,直到前一個訊息被處理完為止。

目前的共識是不應該讓一個應用系統有可能佔用整個系統,而這需要非序列化的訊息佇列,32位元版本的Windows支援這種訊息佇列。如果一個程式正在忙著處理一項冗長作業,那麼您可以將輸入焦點切換到另一個程式中。

多執行緒解決方案

我討論OS/2的Presentation Manager,只是因為它是第一個為早期的Windows程式寫作者(比如我自己)介紹多執行緒的環境。有趣的是,PM實作多執行緒的侷限為程式寫作者提供了應該如何架構多執行緒程式的必要線索。即使這些限制在32位元的Windows中已經大幅減少,但是從更有限的環境中學到的經驗仍然是非常有效的。因此,讓我們繼續討論下去。

在一個多執行緒環境中,程式可以將它們自己分隔為同時執行的片段(叫做執行緒)。對執行緒的支援是解決PM中存在的序列化訊息佇列的最好方法,並且在Windows中執行緒有更實際的意義。

就程式碼來說,一個執行緒簡單地被表示為可能呼叫程式中其他函式的函式。程式從其主執行緒開始執行,這個主執行緒是在傳統的C程式中叫做main的函式,而在Windows中是WinMain。一旦執行起來,程式可以通過在系統呼叫CreateThread中指定初始執行緒函式的名稱來建立新的執行緒的執行。作業系統在執行緒之間優先權式地切換控制項,和它在程序之間切換控制權的方法非常類似。

在OS/2的Presentation Manager中,每個執行緒可以建立一個訊息佇列,也可以不建立。如果希望從執行緒建立視窗,那麼一個PM執行緒必須建立訊息佇列。否則,如果只是進行許多的資料處理或者圖形輸出,那麼執行緒不需要建立訊息佇列。因為無訊息佇列的程序不處理訊息,所以它們將不會當住系統。唯一的限制是一個無訊息佇列執行緒無法向一個訊息佇列執行緒中的視窗發送訊息,或者呼叫任何發送訊息的函式(不過,它們可以將訊息遞送給訊息佇列執行緒)。

這樣,PM程式寫作者學會了如何將它們的程式分隔為一個訊息佇列執行緒(在其中建立所有的視窗並處理傳送給視窗的訊息)和一個或者多個無訊息佇列執行緒,在其中執行冗長的背景工作。PM程式寫作者還瞭解到「1/10秒規則」,大體上,程式寫作者被告知,一個訊息佇列執行緒處理任何訊息都不應該超過1/10秒,任何花費更長時間的事情都應該在另一個執行緒中完成。如果所有的程式寫作者都遵循這一規則,那麼將沒有PM程式會將系統當住超過1/10秒。

多執行緒架構

我已經說過PM的限制讓程式寫作者理解如何在圖形環境中執行的程式裏頭使用多個執行緒提供了必要的線索。因此在這裏我將為您的程式建議一種架構:您的主執行緒建立您程式所需要的所有視窗,並在其中包含所有的視窗訊息處理程式,以便處理這些視窗的所有訊息;所有其他執行緒只進行一些背景處理,除了和主執行緒通訊,它們不和使用者進行交流。

可以把這種架構想像成:主執行緒處理使用者輸入(和其他訊息),並建立程序中的其他執行緒,這些附加的執行緒完成與使用者無關的工作。

換句話說,您程式的主執行緒是一個老闆,而您的其他執行緒是老闆的職員。老闆將大的工作丟給職員處理,而他自己保持和外界的聯繫。因為那些執行緒僅僅是職員,所以其他執行緒不會舉行它們自己的記者招待會。它們會認真地完成自己的工作,將結果報告給老闆,並等待他們的下一個任務。

一個程式中的執行緒是同一程序的不同部分,因此他們共用程序的資源,如記憶體和打開的檔案。因為執行緒共用程式的記憶體,所以他們還共用靜態變數。然而,每個執行緒都有他們自己的堆疊,因此動態變數對每個執行緒是唯一的。每個執行緒還有各自的處理器狀態(和數學輔助運算器狀態),這個狀態在進行執行緒切換期間被儲存和恢復。

執行緒間的「爭吵」

正確地設計、寫作和測試一個複雜的多執行緒應用程式顯然是Windows程式寫作者可能遇到的最困難的工作之一。因為優先權式多工系統可以在任何時刻中斷一個執行緒,並將控制權切換到另一個執行緒中,在兩個執行緒之間可能有無法預料的隨機交互作用的情況。

多執行緒程式中的一個常見的錯誤被稱為「競爭狀態(race condition)」,這發生在程式寫作者假設一個執行緒在另一個執行緒需要某資料之前已經完成了某些處理(如準備資料)的時候。為了幫助協調執行緒的活動,作業系統要求各種形式的同步。一種是同步信號(semaphore),它允許程式寫作者在程式碼中的某一點阻止一個執行緒的執行,直到另一個執行緒發信號讓它繼續為止。類似於同步信號的是「臨界區域(critical section)」,它是程式碼中不可中斷的部分。

但是同步信號還可能產生稱為「鎖死(deadlock)」的常見執行緒錯誤,這發生在兩個執行緒互相阻止了另一個的執行,而繼續執行的唯一辦法又是它們繼續向前執行。

幸運的是,32位元程式比16位元程式更能抵抗執行緒所涉及的某些問題。例如,假定一個執行緒執行下面的簡單敘述:

其中lCount是由其他執行緒使用的一個32位元的long型態變數,C中的這個敘述被編譯為兩條機械碼指令,第一條將變數的低16位元加1,而第二條指令將任何可能的進位加到高16位上。假定作業系統在這兩個機械碼指令之間中斷了執行緒。如果lCount在第一條機械碼指令之前是0x0000FFFF,那麼lCount在執行緒被中斷時為0,而這正是另一個執行緒將看到的值。只有當執行緒繼續執行時,lCount才會增加到正確的值0x00010000。

這是那些偶爾會導致操作問題的錯誤之一。在16位元程式中,解決此問題正確的方法是將敘述包含在一個臨界區域中,在這期間執行緒不會被中斷。然而,在一個32位元程式中,該敘述是正確的,因為它被編譯為一條機械碼指令。

Windows的好處

32位元Windows版本(包括Windows NT和Windows 98)有一個非序列化的訊息佇列。這種實作似乎非常好:如果一個程式正在花費一段長時間處理一個訊息,那麼滑鼠位於該程式的視窗上時,滑鼠游標將呈現為一個時鐘,但是當將滑鼠移到另一個程式的視窗上時,滑鼠游標將變為正常的箭頭形狀。只需按一下就可以將另一個視窗提到前面來。

然而,使用者仍然不能使用正在處理大量工作的那個程式,因為那些工作會阻止程式接收其他訊息,這不是我們所希望的。一個程式應該總是能隨時處理訊息的,所以這時就需要使用從屬執行緒了。

在Windows NT和Windows 98中,沒有訊息佇列執行緒和無訊息佇列執行緒的區別,每個執行緒在建立時都會有它自己的訊息佇列,從而減少了PM程式中關於執行緒的一些不便規定(然而,在大多數情況下,您仍然想通過一條專門處理訊息的執行緒中的訊息程序處理輸入,而將冗長作業交給那些不包含視窗的執行緒處理,這種結構幾乎總是最容易理解的,我們將看到這一點)。

還有更好的事情:Windows NT和Windows 98中有個函式允許執行緒殺死同一程序中的另一個執行緒。當您開始編寫多執行緒程式碼時,您將會發現這種功能在有時是很方便的。OS/2的早期版本沒有「殺死執行緒」的函式。

最後的好訊息(至少對這裏的話題是好訊息)是Windows NT和Windows 98實作了一些被稱為「執行緒區域儲存空間(TLS:thread local storage)」的功能。為了了解這一點,回顧一下我在前面提到過的,靜態變數(對一個函式來說,既是整體又是區域變數)在執行緒之間是被共用的,因為它們位於程序的資料儲存空間中。動態變數(對一個函式來說總是區域變數)對每一個執行緒則是唯一的,因為它們佔據堆疊上的空間,而每個執行緒都有它自己的堆疊。

有時讓兩個或多個執行緒使用相同的函式,而讓這些執行緒使用唯一於執行緒的靜態變數,那會帶來很大便利。這就是執行緒區域儲存空間,其中涉及一些Windows函式呼叫,但是Microsoft還為C編譯器進行擴展,使執行緒區域儲存空間的使用更透明于程式寫作者。

新改良過的!支援多執行緒了!

既然已經介紹了執行緒的現狀,讓我們來展望一下執行緒的未來。有時,有人會出現一種使用作業系統所提供的每一種功能特性的衝動。最壞的情況是,當您的老闆走到您的桌前並說:「我聽說這種新功能非常炫,讓我們在自己的程式中用一些這種新功能吧。」然後您將花費一個星期的時間,試圖去瞭解您的應用程式如何從這種新功能獲益。

應該注意的是,在並不需要多執行緒的應用系統中加入多執行緒是沒有任何意義的。如果您的程式顯示沙漏游標的時間太長,或者如果它使用PeekMessage呼叫來避免沙漏游標的出現,那麼請重新規劃您的程式架構,使用多執行緒可能會是一個好主意。其他情形,您是在為難您自己,並可能會在程式碼中產生新的錯誤。

在某些情況下,沙漏游標的出現可能是完全適當的。我在前面提到過「1/10秒規則」,而將一個大檔案載入記憶體可能會花費多於1/10秒的時間,這是否意味著檔案載入常式應該在分離的執行緒中實作呢?沒有必要。當使用者命令一個程式打開檔案時,他或者她通常想立即完成該操作。將檔案載入常式放在分離的執行緒中只會增加額外的負擔。即使您想向您的朋友誇耀您在編寫多執行緒程式,也完全不值得這樣做!

WINDOWS的多執行緒處理

建立新的執行緒的API函式是CreateThread,它的語法如下:

第一個參數是指向SECURITY_ATTRIBUTES型態的結構的指標。在Windows 98中忽略該參數。在Windows NT中,它被設為NULL。第二個參數是用於新執行緒的初始堆疊大小,預設值為0。在任何情況下,Windows根據需要動態延長堆疊的大小。

CreateThread的第三個參數是指向執行緒函式的指標。函式名稱沒有限制,但是必須以下列形式宣告:

CreateThread的第四個參數為傳遞給ThreadProc的參數。這樣主執行緒和從屬執行緒就可以共用資料。

CreateThread的第五個參數通常為0,但當建立的執行緒不馬上執行時為旗標CREATE_SUSPENDED。執行緒將暫停直到呼叫ResumeThread來恢復執行緒的執行為止。第六個參數是一個指標,指向接受執行緒ID值的變數。

大多數Windows程式寫作者喜歡用在PROCESS.H表頭檔案中宣告的C執行時期程式庫函式_beginthread。它的語法如下:

它更簡單,對於大多數應用程式很完美,這個執行緒函式的語法為:

再論隨機矩形

程式20-1 RNDRCTMT是 第五章裏的RANDRECT程式 的多執行緒版本,您將回憶起RANDRECT使用的是PeekMessage迴圈來顯示一系列的隨機矩形。

lCount++ ;

hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc, pParam, dwFlags, &idThread) ;

DWORD WINAPI ThreadProc (PVOID pParam) ;

hThread = _beginthread (ThreadProc, uiStackSize, pParam) ;

void __cdecl ThreadProc (void * pParam) ;

程式20-1 RNDRCTMT RNDRCTMT.C /*--------------------------------------------------------------------------- RNDRCTMT.C -- Displays Random Rectangles (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #include <process.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HWND hwnd ; int cxClient, cyClient ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("RndRctMT") ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Random Rectangles"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } VOID Thread (PVOID pvoid) { HBRUSH hBrush ; HDC hdc ; int xLeft, xRight, yTop, yBottom, iRed, iGreen, iBlue ; while (TRUE) { if (cxClient != 0 || cyClient != 0) { xLeft = rand () % cxClient ; xRight = rand () % cxClient ; yTop = rand () % cyClient ; yBottom = rand () % cyClient ; iRed = rand () & 255 ; iGreen = rand () & 255 ; iBlue = rand () & 255 ; hdc = GetDC (hwnd) ; hBrush = CreateSolidBrush (RGB (iRed, iGreen, iBlue)) ; SelectObject (hdc, hBrush) ; Rectangle (hdc, min (xLeft, xRight), min (yTop, yBottom), max (xLeft, xRight), max (yTop, yBottom)) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; } } } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CREATE: _beginthread (Thread, 0, NULL) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

在建立多執行緒的Windows程式時,需要在「Project Settings」對話方塊中做一些修改。選擇「C/C++」頁面標籤,然後在「Category」下拉式清單方塊中選擇「Code Generation」。在「Use Run-Time Library」下拉式清單方塊中,可以看到用於「Release」設定的「Single-Threaded」和用於Debug設定的「Debug Single-Threaded」。將這些分別改為「Multithreaded」和「Debug Multithreaded」。這將把編譯器旗標改為/MT,它是編譯器在編譯多執行緒的應用程式所需要的。具體地說,編譯器將在.OBJ檔案中插入LIBCMT.LIB檔案名,而不是LIBC.LIB。連結程式使用這個名稱與執行期程式庫函式連結。

LIBC.LIB和LIBCMT.LIB檔案包含C語言程式庫函式,有些C語言程式庫函式包含靜態資料。例如,由於strtok函式可能被連續地多次呼叫,所以它在靜態記憶體中儲存了一個指標。在多執行緒程式中,每個執行緒必須在strtok函式中有它自己的靜態指標。因此,這個函式的多執行緒版本稍微不同於單執行緒的strtok函式。

同時請注意,我在RNDRCTMT.C中包含了表頭檔案PROCESS.H,這個檔案定義一個名為_beginthread的函式,它啟動一個新的執行緒。只有定義了_MT識別字,才會宣告這個函式,這是/MT旗標的另一個結果。

在RNDRCTMT.C的WinMain函式中,由CreateWindow傳回的hwnd值被儲存在一個整體變數中,因此cxClient和cyClient值也可以由視窗訊息處理程式的WM_SIZE訊息獲得。

視窗訊息處理程式以最容易的方法呼叫_beginthread-簡單地以執行緒函式的位址(稱為Thread)作為第一個參數,其他參數使用0,執行緒函式傳回VOID並有一個參數,該參數是一個指向VOID的指標。在RNDRCTMT中的Thread函式不使用這個參數。

在呼叫了_beginthread函式之後,執行緒函式(以及該執行緒函式可能呼叫的其他任何函式)中的程式碼和程式中的其他程式碼同時執行。兩個或者多個執行緒使用一個程序中的同一函式,在這種情況下,動態區域變數(儲存在堆疊上)對每個執行緒是唯一的。對程序中的所有執行緒來說,所有的靜態變數都是一樣的。這就是視窗訊息處理程式設定整體的cxClient和cyClient變數並由Thread函式使用的方式。

有時您需要唯一於各個執行緒的持續儲存性資料。通常,這種資料是靜態變數,但在Windows 98中,您可以使用「執行緒區域儲存空間」,我將在本章後面進行討論。

程式設計競賽的問題

1986年10月3日,Microsoft舉行了為期一天,針對電腦雜誌出版社的技術編輯和作者的簡短的記者招待會,來討論他們當時的一組語言產品,包括他們的第一個交談式開發環境,QuickBASIC 2.0。當時,Windows 1.0出現還不到一年,但是沒有人知道我們什麼時候能得到與該環境類似的東西(這花了好幾年)。這一事件與眾不同的部分原因是由於Microsoft的公關人員所舉辦的「Storm the Gates」程式設計競賽。Bill Gates使用QuickBASIC 2.0,而電腦出版社的人員可以使用他們選擇的任何語言產品。

競賽的問題是從公眾提出的題目中挑選出來的(挑選那些需要寫大約半小時程式來解決的問題),問題如下:

建立一個包含四個視窗的多工模擬程式。第一個視窗必須顯示一系列的遞增數,第二個必須顯示一系列的遞增質數,而第三個必須顯示Fibonacci數列(Fibonacci數列以數字0和1開始,後頭每一個數都是其前兩個數的和-即0、1、1、2、3、5、8等等)。這三個視窗應該在數字達到視窗底部時或者進行滾動,或者自行清除視窗內容。第四個視窗必須顯示任意半徑的圓,而程式必須在按下一個Escape鍵時終止。

當然,在1986年10月,在DOS下執行的這樣一個程式最多只能是模擬多工而已,而且沒有一個競賽者具有足夠的勇氣-並且其中大多數也沒有足夠的知識-來為Windows編寫這個程式。再者,如果真要這麼做,當然不會只花半小時了!

參加這次競賽的大多數人編寫了一個程式來將螢幕分為四個區域,程式中包含一個迴圈,依次更新每個視窗,然後檢查是否按下了Escape鍵。如同DOS環境下的傳統習慣,程式佔用了百分之百的CPU處理時間。

如果在Windows 1.0中寫程式,那麼結果將是類似程式20-2 MULTI1的結果。我說「類似」,是因為我編寫的程式是32位元的,但程式結構和相當多的程式碼-除了變數和函式參數定義以及Unicode支援-都是相同的。

程式20-2 MULTI1 MULTI1.C /*-------------------------------------------------------------------------- MULTI1.C -- Multitasking Demo (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int cyChar ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Multi1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Multitasking Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } int CheckBottom (HWND hwnd, int cyClient, int iLine) { if (iLine * cyChar + cyChar > cyClient) { InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ; iLine = 0 ; } return iLine ; } // ------------------------------------------------------------------------- // Window 1: Display increasing sequence of numbers // ------------------------------------------------------------------------- LRESULT APIENTRY WndProc1 ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int iNum, iLine, cyClient ; HDC hdc ; TCHAR szBuffer[16] ; switch (message) { case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: if (iNum < 0) iNum = 0 ; iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut (hdc, 0, iLine * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum++)) ; ReleaseDC (hwnd, hdc) ; iLine++ ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 2: Display increasing sequence of prime numbers // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc2 ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int iNum = 1, iLine, cyClient ; HDC hdc ; int i, iSqrt ; TCHAR szBuffer[16] ; switch (message) { case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: do { if (++iNum < 0) iNum = 0 ; iSqrt = (int) sqrt (iNum) ; for (i = 2 ; i <= iSqrt ; i++) if (iNum % i == 0) break ; } while (i <= iSqrt) ; iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut ( hdc, 0, iLine * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum)) ; ReleaseDC (hwnd, hdc) ; iLine++ ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 3: Display increasing sequence of Fibonacci numbers // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc3 ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static int iNum = 0, iNext = 1, iLine, cyClient ; HDC hdc ; int iTemp ; TCHAR szBuffer[16] ; switch (message) { case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: if (iNum < 0) { iNum = 0 ; iNext = 1 ; } iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut ( hdc, 0, iLine * cyChar, szBuffer, wsprintf (szBuffer, "%d", iNum)) ; ReleaseDC (hwnd, hdc) ; iTemp = iNum ; iNum = iNext ; iNex += iTemp ; iLine++ ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 4: Display circles of random radii // --------------------------------------------------------------------------- LRESULT APIENTRY WndProc4 ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; int iDiameter ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ; iDiameter = rand() % (max (1, min (cxClient, cyClient))) ; hdc = GetDC (hwnd) ; Ellipse (hdc, (cxClient - iDiameter) / 2, (cyClient - iDiameter) / 2, (cxClient + iDiameter) / 2, (cyClient + iDiameter) / 2) ; ReleaseDC (hwnd, hdc) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Main window to create child windows // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndChild[4] ; static TCHAR * szChildClass[] = { TEXT ("Child1"), TEXT ("Child2"), TEXT ("Child3"), TEXT ("Child4") } ; static WNDPROC ChildProc[] = { WndProc1, WndProc2, WndProc3, WndProc4 } ; HINSTANCE hInstance ; int i, cxClient, cyClient ; WNDCLASS wndclass ; switch (message) { case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; for (i = 0 ; i < 4 ; i++) { wndclass.lpfnWndProc = ChildProc[i] ; wndclass.lpszClassName = szChildClass[i] ; RegisterClass (&wndclass) ; hwndChild[i] = CreateWindow (szChildClass[i], NULL, WS_CHILDWINDOW | WS_BORDER | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) i, hInstance, NULL) ; } cyChar = HIWORD (GetDialogBaseUnits ()) ; SetTimer (hwnd, 1, 10, NULL) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; for (i = 0 ; i < 4 ; i++) MoveWindow (hwndChild[i], (i % 2) * cxClient / 2, (i > 1) * cyClient / 2, cxClient / 2, cyClient / 2, TRUE) ; return 0 ; case WM_TIMER: for (i = 0 ; i < 4 ; i++) SendMessage (hwndChild[i], WM_TIMER, wParam, lParam) ; return 0 ; case WM_CHAR: if (wParam == '\x1B') DestroyWindow (hwnd) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, 1) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

在這個程式裏實際上沒有什麼我們沒見過的東西。主視窗建立四個子視窗,每個子視窗佔據顯示區域的一個象限。主視窗還設定一個Windows計時器並發送WM_TIMER訊息給四個子視窗中的每一個。

通常一個Windows程式應該保留足夠的資訊以便在WM_PAINT訊息處理期間重建其視窗中的內容。MULTI1沒有這麼做,既然它繪製和清除視窗的速度如此之快,所以我認為那是不必要的。

WndProc2中的質數產生器的效率並不很高,但是有效。如果一個數除了1和它自身以外沒有別的因數,那麼這個數就是質數。當然,要檢查一個數是否是質數並不要求使用小於被檢查數的所有數來除這個數並檢查餘數,而只需使用所有小於被檢查數的平方根的數。平方根計算是發表浮點數的原因,否則,該程式將是完全依據整數的程式。

MULTI1程式沒有什麼不好的地方。使用Windows計時器是在Windows的早期(和目前)版本中模擬多工的一種好方法,然而,計時器的使用有時限制了程式的速度。如果程式可以在WM_TIMER訊息處理中更新它的所有視窗而還有時間剩餘下來的話,那就意味著它並沒有充分利用我們的機器資源。

一種可能的解決方案是在單個WM_TIMER訊息處理期間進行兩次或者更多次的更新,但是到底多少次呢?這不得不依賴於機器的速度,而有很大的變動性。您當然不會想編寫一個只能適用於25MHz的386或50MHz的486或100-GHz的Pentium VII上的程式吧。

多執行緒解決方案

讓我們來看一看關於這個程式設計問題的一種多執行緒解決方案。如程式20-3 MULTI2所示。

程式20-3 MULTI2 MULTI2.C /*--------------------------------------------------------------------------- MULTI2.C -- Multitasking Demo (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> typedef struct { HWND hwnd ; int cxClient ; int cyClient ; int cyChar ; BOOL bKill ; } PARAMS, *PPARAMS ; LRESULT APIENTRY WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Multi2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Multitasking Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } int CheckBottom (HWND hwnd, int cyClient, int cyChar, int iLine) { if (iLine * cyChar + cyChar > cyClient) { InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ; iLine = 0 ; } return iLine ; } // -------------------------------------------------------------------------- // Window 1: Display increasing sequence of numbers // -------------------------------------------------------------------------- void Thread1 (PVOID pvoid) { HDC hdc ; int iNum = 0, iLine = 0 ; PPARAMS pparams ; TCHAR szBuffer[16] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { if (iNum < 0) iNum = 0 ; iLine = CheckBottom ( pparams->hwnd, pparams->cyClient, pparams->cyChar, iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut ( hdc, 0, iLine * pparams->cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum++)) ; ReleaseDC (pparams->hwnd, hdc) ; iLine++ ; } _endthread () ; } LRESULT APIENTRY WndProc1 ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread1, 0, ¶ms) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 2: Display increasing sequence of prime numbers // -------------------------------------------------------------------------- void Thread2 (PVOID pvoid) { HDC hdc ; int iNum = 1, iLine = 0, i, iSqrt ; PPARAMS pparams ; TCHAR szBuffer[16] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { do { if (++iNum < 0) iNum = 0 ; iSqrt = (int) sqrt (iNum) ; for (i = 2 ; i <= iSqrt ; i++) if (iNum % i == 0) break ; } while (i <= iSqrt) ; iLine = CheckBottom ( pparams->hwnd, pparams->cyClient, pparams->cyChar, iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut ( hdc, 0, iLine * pparams->cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum)) ; ReleaseDC (pparams->hwnd, hdc) ; iLine++ ; } _endthread () ; } LRESULT APIENTRY WndProc2 ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread2, 0, ¶ms) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // Window 3: Display increasing sequence of Fibonacci numbers // ---------------------------------------------------------- void Thread3 (PVOID pvoid) { HDC hdc ; int iNum = 0, iNext = 1, iLine = 0, iTemp ; PPARAMS pparams ; TCHAR szBuffer[16] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { if (iNum < 0) { iNum = 0 ; iNext = 1 ; } iLine = CheckBottom ( pparams->hwnd, pparams->cyClient, pparams->cyChar, iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut (hdc, 0, iLine * pparams->cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum)) ; ReleaseDC (pparams->hwnd, hdc) ; iTemp = iNum ; iNum = iNext ; iNext += iTemp ; iLine++ ; } _endthread () ; } LRESULT APIENTRY WndProc3 ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread3, 0, ¶ms) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // ------------------------------------------------------------------------- // Window 4: Display circles of random radii // ------------------------------------------------------------------------- void Thread4 (PVOID pvoid) { HDC hdc ; int iDiameter ; PPARAMS pparams ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { InvalidateRect (pparams->hwnd, NULL, TRUE) ; UpdateWindow (pparams->hwnd) ; iDiameter = rand() % (max (1, min (pparams->cxClient, pparams->cyClient))) ; hdc = GetDC (pparams->hwnd) ; Ellipse (hdc, (pparams->cxClient - iDiameter) / 2, (pparams->cyClient - iDiameter) / 2, (pparams->cxClient + iDiameter) / 2, (pparams->cyClient + iDiameter) / 2) ; ReleaseDC (pparams->hwnd, hdc) ; } _endthread () ; } LRESULT APIENTRY WndProc4 (HWND hwnd, UINT message,WPARAM wParam,LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread4, 0, ¶ms) ; return 0 ; case WM_SIZE: params.cxClient = LOWORD (lParam) ; params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Main window to create child windows // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndChild[4] ; static TCHAR * szChildClass[] = { TEXT ("Child1"), TEXT ("Child2"), TEXT ("Child3"), TEXT ("Child4") } ; static WNDPROC ChildProc[] = { WndProc1, WndProc2, WndProc3, WndProc4 } ; HINSTANCE hInstance ; int i, cxClient, cyClient ; WNDCLASS wndclass ; switch (message) { case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; for (i = 0 ; i < 4 ; i++) { wndclass.lpfnWndProc = ChildProc[i] ; wndclass.lpszClassName = szChildClass[i] ; RegisterClass (&wndclass) ; hwndChild[i] = CreateWindow (szChildClass[i], NULL, WS_CHILDWINDOW | WS_BORDER | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) i, hInstance, NULL) ; } return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; for (i = 0 ; i < 4 ; i++) MoveWindow (hwndChild[i], (i % 2) * cxClient / 2, (i > 1) * cyClient / 2, cxClient / 2, cyClient / 2, TRUE) ; return 0 ; case WM_CHAR: if (wParam == '\x1B') DestroyWindow (hwnd) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

MULTI2.C的WinMain和WndProc函式非常類似於MULTI1.C中的同名函式。WndProc為四個視窗註冊了四種視窗類別,建立了這些視窗,並在WM_SIZE訊息處理期間縮放這些視窗。WndProc的唯一不同是它不再設定Windows計時器,也不再處理WM_TIMER訊息。

MULTI2中較大的改變是每個子視窗訊息處理程式透過在WM_CREATE訊息處理期間呼叫_beginthread函式來建立另一個執行緒。總括來說,MULTI2程式有五個同時執行的執行緒,主執行緒包含主視窗訊息處理程式和四個子視窗訊息處理程式,其餘的四個執行緒使用名為Thread1、Thread2等的函式,這四個執行緒負責繪製四個視窗。

我在RNDRCTMT程式中給出的多執行緒程式碼沒有使用_beginthread的第三個參數,這個參數允許一個建立另一個執行緒的執行緒在32位元變數中將資訊傳遞給其他執行緒。通常,這個變數是一個指標,而且是指向一個結構的指標,這允許原來的執行緒和新執行緒共用資訊,而不必借助於整體變數。您可以看到,在MULTI2中沒有整體變數。

對MULTI2程式,我在程式開頭定義了一個名為PARAMS的結構和一個名為PPARAMS的指向結構的指標,這個結構有五個欄位-視窗代號、視窗的寬度和高度、字元的高度和名為bKill的布林變數。最後的結構欄位允許建立執行緒告知被建立執行緒何時終止。

讓我們來看一看WndProc1,這是顯示增加數序列的子視窗訊息處理程式。視窗訊息處理程式變得非常簡單,唯一的區域變數是一個PARAMS結構。在WM_CREATE訊息處理期間,它設定這個結構的hwnd和cyChar欄位,呼叫_beginthread來建立一個使用Thread1函式的新執行緒,並傳遞給新執行緒一個指向該結構的指標。在WM_SIZE訊息處理期間,WndProc1設定結構的cyClient欄位,而在WM_DESTROY訊息處理期間,它將bKill欄位設定為TRUE。Thread1函式通過對_endthread的呼叫而告結束。這並不是絕對必要的,因為執行緒將在退出執行緒函式之後被清除。不過,要退出一個深陷入複雜的處理程序的執行緒時,_endthread是很有用的。

Thread1函式完成在視窗上的實際繪圖,並且和程式的其他四個執行緒同時執行。函式接收指向PARAMS結構的一個指標,並進入一個while迴圈,不斷檢查bKill是TRUE還是FALSE。如果是FALSE,那麼函式必須進行MULTI1.C中的WM_TIMER訊息處理期間所作的同樣處理-格式化數字、取得裝置內容代號並使用TextOut顯示數字。

當您在Windows 98中執行MULTI2時,將會看到,視窗更新要比在MULTI1中快得多,這表示程式在更加有效地利用處理器的資源。在MULTI1和MULTI2之間還有另一種區別:通常,當您移動或者縮放一個視窗時,內定視窗訊息處理程式進入一種模態迴圈,而視窗的所有輸出都將停止。在MULTI2中,輸出將繼續。

有問題嗎?

似乎MULTI2程式並沒有達到它應該有的穩固性。我為什麼會這樣認為呢?讓我們來看一看MULTI2.C中的一些多執行緒「缺陷」,以WndProc1和Thread1為例。

WndProc1在MULTI2的主執行緒中執行,而Thread1與它同時執行,Windows 98在這兩個執行緒之間進行切換是不可預測的。假定Thread1正在執行,並且剛好執行了檢查PARAMS結構的bKill欄位是否為TRUE的程式碼。發現不為TRUE,但是這之後Windows 98將控制權切換到主執行緒,這時使用者終止了程式,WndProc1收到一個WM_DESTROY訊息並將bKill參數設為TRUE。哦,這參數設定得太晚了!作業系統突然切換到Thread1中,而該函式會試圖取得一個不存在的視窗的裝置內容代號。

事實證明,這不是一個問題。Windows 98夠穩固,以致另一條執行緒呼叫的圖形處理函式只是失敗而已,而不會引起任何問題。

正確的多執行緒程式寫作技術涉及執行緒同步的使用(尤其是臨界區域的使用),我將馬上加以詳細地討論。大體上,臨界區域通過對EnterCriticalSection和LeaveCriticalSection的呼叫而加以界定。如果一個執行緒進入一個臨界區域,那麼另一個執行緒將無法再進入這個臨界區域。後一個執行緒被阻檔在對EnterCriticalSection的呼叫上,直到第一個執行緒呼叫LeaveCriticalSection時為止。

在MULTI2中的另一個可能存在的問題是,當另外一個執行緒顯示其輸出時,主執行緒可能會收到一個WM_ERASEBKGND或WM_PAINT訊息。這裏,使用臨界區域有助於避免當兩個程序試圖在同一個視窗上繪圖時可能導致的任何問題。但是,經驗顯示,Windows 98很恰當地序列化了對圖形繪製函式的存取。亦即,當另一個執行緒正在繪圖的時候,一個執行緒不能在同一個視窗上繪圖。

Windows 98文件提醒說,有一種未進行圖形函式序列化的情形,這就是GDI物件(如畫筆、畫刷、字體、點陣圖、區域和調色盤等)的使用。有可能發生一個執行緒清除了一個物件,而另一個執行緒仍然在使用它的情況。解決這個問題的方法要求使用臨界區域,或者最好不要在執行緒之間共用GDI物件。

Sleep的好處

我曾經提到,我認為對一個多執行緒程式來說,最好的架構是主執行緒建立程式中的所有視窗,以及所有的視窗訊息處理程式,並處理所有的視窗訊息。其他執行緒完成背景工作或者冗長作業。

不過,假設您想在另一個執行緒中做動畫。通常,Windows中的動畫是使用WM_TIMER訊息來實作的。如果這個執行緒沒有建立視窗,那麼它也不會收到這些訊息。如果沒有計時器,動畫又可能會執行得太快。

解決方案是Sleep函式。實際上,執行緒呼叫Sleep函式來自動暫停執行,該函式唯一一個參數是以毫秒計的時間。Sleep函式呼叫在指定的時間過去以前不會傳回控制權。在這段時間內,執行緒被暫停,並且不會被配置給時間片段(儘管該執行緒顯然仍然要求在tick時給予一小段的處理時間,因為系統必須確定執行緒是否應該重新開始執行)。給Sleep一個值為0的參數將導致執行緒交回它尚未使用完的時間片段。

當一個執行緒呼叫Sleep時,只是該執行緒被暫停指定的時間。系統仍然執行其他的執行緒,這些執行緒和暫停的執行緒可以是在同一個程序中,也可以是在另一個程序中。我在第十四章中的SCRAMBLE程式中使用了Sleep函式,以放慢畫面清除的操作。

通常,您不應該在您的主執行緒中使用Sleep函式,因為這會減慢對訊息的處理速度,但是因為SCRAMBLE沒有建立任何視窗,因此在那裏使用Sleep應該沒有問題。

執行緒同步

大約每年一次,在我公寓窗外的交通繁忙地段的紅綠燈會停止工作。結果是造成交通的混亂,雖然轎車一般能避免撞上別的轎車,但是這些車經常擠在一起。

我用術語稱兩條路相交的十字路口為「臨界區域」。一輛向南的車和一輛向西的車不可能同時通過一個十字路口而不撞著對方。依賴於交通流量,可以採用不同的方法來解決這個問題。對於視野清楚車輛稀少的路口,可以相信司機有處理的能力。車輛增多可能會要求一個停車號誌,而更加繁忙的交通則將要求有紅綠燈,紅綠燈有助於協調路口的交通(當然,這些燈號必須正常工作)。

臨界區域

在單工作業系統中,傳統的電腦程式不需要紅綠燈來幫助協調它們之間的行為。它們在執行時似乎獨佔了整條路,而且也確實是這樣,沒有什麼會干擾它們的工作。

即使在多工作業系統中,大多數的程式也似乎各自獨立地在執行,但是可能會發生一些問題。例如,兩個程式可能會需要同時從同一個檔案中讀或者對同一檔案進行寫。在這種情況下,作業系統提供了一種共用檔案和記錄上鎖的技術來幫助解決這個問題。

然而,在支援多執行緒的作業系統中,情況會變得混亂而且存在潛在的危險。兩個或多個執行緒共用某些資料的情況並不罕見。例如,一個執行緒可以更新一個或者多個變數,而另一個執行緒可以使用這些變數。有時這會引發一個問題,有時又不會(記住作業系統將控制權從一個執行緒切換到另一個執行緒的操作,只能在機器碼指令之間發生。如果只是一個整數被執行緒共用,那麼對這個變數的改變通常發生在單個指令中,因此潛在的問題被最小化了)。

然而,假設執行緒共用幾個變數或者資料結構。通常,這麼多個變數或者結構的欄位在它們之間必須是一致的。作業系統可以在更新這些變數的程序中間中斷一個執行緒,那麼使用這些變數的執行緒得到的將是不一致的資料。

結果是衝突發生了,並且通常不難想像這樣的錯誤將對程式造成怎樣的破壞。我們所需要的是類似於紅綠燈的程式寫作技術,以幫助我們對執行緒交通進行協調和同步,這就是臨界區域。大體上,一個臨界區域就是一塊不可中斷的程式碼。

有四個函式用於臨界區域。要使用這些函式,您必須定義一個臨界區域物件,這是一個型態為CRITICAL_SECTION的整體變數。例如:

這個CRITICAL_SECTION資料型態是一個結構,但是其中的欄位只能由Windows內部使用。這個臨界區域物件必須先被程式中的某個執行緒初始化,通過呼叫:

這樣就建立了一個名為cs的臨界區域物件。該函式的線上輔助說明包含下面的警告:「臨界區域物件不能被移動或者複製,程序也不能修改該物件,但必須在邏輯上把它視為不透明的。」這句話,可以被解釋為:「不要干擾它,甚至不要看它。」

當臨界區域物件被初始化之後,執行緒可以通過下面的呼叫進入臨界區域:

在這時,執行緒被認為「擁有」臨界區域物件。兩個執行緒不可以同時擁有同一個臨界區域物件,因此,如果一個執行緒進入了臨界區域,那麼下一個使用同一臨界區域物件呼叫EnterCriticalSection的執行緒將在函式呼叫中被暫停。只有當第一個執行緒通過下面的呼叫離開臨界區域時,函式才會傳回控制權:

這時,在EnterCriticalSection呼叫中被停住的那個執行緒將擁有臨界區域,其函式呼叫也將傳回,允許執行緒繼續執行。

當臨界區域不再被程式所需要時,可以通過呼叫

將其刪除,該函式釋放所有被配置來維護此臨界區域物件的系統資源。

這種臨界區域技術涉及「互斥」(此術語在我們繼續討論執行緒同步時將再次出現)。在任何時刻,只有一個執行緒能擁有一個臨界區域。因此,一個執行緒可以進入一個臨界區域,設定一個結構的欄位,然後退出臨界區域。另一個使用該結構的執行緒在存取結構中的欄位之前也要先進入該臨界區域,然後再退出臨界區域。

注意,您可以定義多個臨界區域物件,比如cs1和cs2。例如,如果一個程式有四個執行緒,而前兩個執行緒共用一些資料,那麼它們可以使用一個臨界區域物件,而另外兩個執行緒共用一些其他的資料,那麼它們可以使用另一個臨界區域物件。

您在主執行緒中使用臨界區域時應該小心。如果從屬執行緒在它自己的臨界區域中花費了一段很長的時間,那麼它可能會將主執行緒的執行阻礙很長一段時間。從屬執行緒可能只是使用臨界區域複製該結構的欄位到自己的區域變數中。

臨界區域的一個限制是它們只能用於在同一程序內的執行緒之間的協調。但是在某些情況下,您需要協調兩個不同程序對同一資源的共用(如共用記憶體等)。在此其況下不能使用臨界區域,但是可以使用一種被稱為「互斥物件(mutex object)」的技術。「mutex」是個合成字,代表「mutual exclusion(互斥)」,它在這裏精確地表達了我們的目的。我們想防止一個程式的執行緒在更新資料或者使用共用記憶體與其他資源時被中斷。

事件信號

多執行緒通常是用於那些必須執行長時間處理的程式。我們可以將一個「大作業」定義為一個可能會違反1/10秒規則的程式。顯然大作業包括文書處理程式中的拼寫檢查、資料庫程式中的檔案排序或者索引、試算表的重新計算、列印,甚至包括複雜的繪圖。當然,迄今為止我們知道,遵循1/10秒規則的最好方法是將大作業放到另一個執行緒去執行。這些額外的執行緒不會建立視窗,因此它們不受1/10秒規則的限制。

通常希望這些額外的執行緒在完成其任務時能夠通知主執行緒,或者主執行緒能夠停止其他執行緒正在進行的作業。這就是我們下面將要討論的。

BIGJOB1程式

作為一個想像的大作業,我將使用一系列浮點運算,有時這種運算被稱為「暴力的」性能測試指標。這種計算以一種間接的方式遞增一個整數的值:它求一個數的平方,再對結果取平方根(得到原來的整數),然後使用log和exp函式(同樣得到原來的整數),接著使用atan和tan函式(還是得到原來的整數),最後對結果加1。

BIGJOB1程式如程式20-4所示。

CRITICAL_SECTION cs ;

InitializeCriticalSection (&cs) ;

EnterCriticalSection (&cs) ;

LeaveCriticalSection (&cs) ;

DeleteCriticalSection (&cs) ;

程式20-4 BIGJOB1 BIGJOB1.C /*--------------------------------------------------------------------------- BIGJOB1.C -- Multithreading Demo (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> #define REP 1000000 #define STATUS_READY 0 #define STATUS_WORKING 1 #define STATUS_DONE 2 #define WM_CALC_DONE (WM_USER + 0) #define WM_CALC_ABORTED (WM_USER + 1) typedef struct { HWND hwnd ; BOOL bContinue ; } PARAMS, *PPARAMS ; LRESULT APIENTRY WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BigJob1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Multithreading Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void Thread (PVOID pvoid) { double A = 1.0 ; INT i ; LONG lTime ; volatile PPARAMS pparams ; pparams = (PPARAMS) pvoid ; lTime = GetCurrentTime () ; for (i = 0 ; i < REP && pparams->bContinue ; i++) A = tan (atan (exp (log (sqrt (A * A))))) + 1.0 ; if (i == REP) { lTime = GetCurrentTime () - lTime ; SendMessage (pparams->hwnd, WM_CALC_DONE, 0, lTime) ; } else SendMessage (pparams->hwnd, WM_CALC_ABORTED, 0, 0) ; _endthread () ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static INT iStatus ; static LONG lTime ; static PARAMS params ; static TCHAR * szMessage[] = { TEXT ("Ready (left mouse button begins)"), TEXT ("Working (right mouse button ends)"), TEXT ("%d repetitions in %ld msec") } ; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; TCHAR szBuffer[64] ; switch (message) { case WM_LBUTTONDOWN: if (iStatus == STATUS_WORKING) { MessageBeep (0) ; return 0 ; } iStatus = STATUS_WORKING ; params.hwnd = hwnd ; params.bContinue = TRUE ; _beginthread (Thread, 0, ¶ms) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_RBUTTONDOWN: params.bContinue = FALSE ; return 0 ; case WM_CALC_DONE: lTime = lParam ; iStatus = STATUS_DONE ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_CALC_ABORTED: iStatus = STATUS_READY ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; wsprintf (szBuffer, szMessage[iStatus], REP, lTime) ; DrawText (hdc, szBuffer, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

這是一個相當簡單的程式,但是我認為您將看到它如何展示在多執行緒程式中完成大作業的通用方法。為了使用BIGJOB1程式,在視窗的顯示區域中按下滑鼠左鍵,從而開始暴力的性能測試計算的1,000,000次重複,這在一台300MHz的Pentium II機器上將花費2秒。當完成計算時,花費的時間將顯示在視窗上。當正在進行計算時,您可以通過在顯示區域中按下滑鼠右鍵來終止它。

讓我們來看一看這是如何實作的:

視窗訊息處理程式擁有了一個被叫做iStatus的靜態變數(該變數可以被設定為在程式開始處定義的三個常數之一,常數以STATUS為字首),該變數表示程式是否準備好進行一次計算,是否正在進行一次計算,或者是否完成了計算。程式在WM_PAINT訊息處理期間使用iStatus變數在顯示區域的中央顯示一個適當的字串。

視窗訊息處理程式還擁有一個靜態結構(型態為PARAMS,也定義在程式的頂部),該結構是在視窗訊息處理程式和其他執行緒之間的共用資料。結構只有兩個欄位-hwnd(程式視窗的代號)和bContinue,這是一個布林變數,用於指示執行緒是否繼續計算或者停止。

當您在顯示區域中按下滑鼠左鍵時,視窗訊息處理程式將iStatus變數設為STATUS_WORKING,並設定PARAMS結構中的兩個欄位。結構的hwnd欄位被設定為視窗代號,當然,bContinue被設定為TRUE。

然後視窗程序呼叫_beginthread函式。執行緒函式Thread以呼叫GetCurrentTime開始,GetCurrentTime取得以毫秒計的Windows啟動以來已經執行了的時間。然後它進入一個for迴圈,重複1,000,000次的暴力測試計算。還要注意,如果bContinue被設為了FALSE,那麼執行緒將退出迴圈。

在for迴圈之後,執行緒函式檢查它是否確實完成了1,000,000次計算。如果是,那麼它再次呼叫GetCurrentTime獲得所經過的時間,然後使用SendMessage向視窗訊息處理程式發送一個由程式定義的WM_USER_DONE訊息,並以經過的時間作為lParam參數。如果計算是在未完成之前被終止的(即,如果在迴圈期間PARAMS結構的bContinue欄位變為FALSE),那麼執行緒將發送給視窗訊息處理程式一個WM_USER_ABORTED訊息。然後,執行緒通過呼叫_endthread正常地結束。

在視窗訊息處理程式中,當您在顯示區域中按下滑鼠右鍵時,PARAMS結構的bContinue欄位被設為FALSE。這是如何在完成計算之前結束計算的方法。

注意Thread中的pparams變數定義為volatile,這種型態限定字向編譯器指出變數可能會在實際的程式敘述外被修改(例如被另一個執行緒)。否則,最佳化的編譯器會假設pparams->bContinue不能被for迴圈內的程式碼修改,沒有必要在每層迴圈中檢查變數。volatile關鍵字防止這樣的最佳化進行。

視窗訊息處理程式處理WM_USER_DONE訊息時,首先儲存經過的時間。對WM_USER_DONE和WM_USER_ABORTED訊息的處理都是透過對InvalidateRect的呼叫產生WM_PAINT訊息並在顯示區域顯示一個新的字串。

提供一個方法(如結構中的bContinue欄位)允許執行緒正常終止,通常是一個好主意。KillThread函式只有在正常終止執行緒比較困難時才應該使用,原因是執行緒可以配置資源,如記憶體等。如果當執行緒終止時沒有釋放所配置的記憶體,那麼記憶體將仍然是被配置了的。執行緒不是程序:所配置的資源在一個程序的所有執行緒之間是共用的,因此當執行緒終止時,資源不會被自動釋放。好的程式結構要求一個執行緒釋放由它配置的所有資源。

您還應該知道當第二個執行緒仍在執行時,可以建立第三個執行緒。如果Windows在SendMessage呼叫和_endthread呼叫之間,將控制權從第二個執行緒切換到第一個執行緒,那麼視窗訊息處理程式就可能回應滑鼠按鍵而建立一個新的執行緒,從而出現了上述的情況。這不是什麼問題,但是如果這對您自己的應用來說是一個問題的話,那麼您可能會考慮使用臨界區域來避免執行緒之間的衝突。

事件物件

BIGJOB1在每次需要執行暴力測試計算時,就建立一個執行緒。執行緒在完成計算之後自動終止。

另一種可用的方法是在程式的整個生命周期內保持執行緒的執行,但是只在必要時才啟動它。這是一個應用事件物件的理想情況。

事件物件可以是「有信號的」(也稱為「被設立的」)或「沒信號的」(也稱為「被重置的」)。您可以通過下面呼叫來建立事件物件:

第一個參數(指向一個SECURITY_ATTRIBUTES結構的指標)和最後一個參數(一個事件物件的名字)只有在事件物件被多個程序共用時才有意義。在同一程序中,這些參數通常被設定為NULL。如果您希望事件物件被初始化為有信號的,那麼將fInitial參數設定為TRUE。而如果希望事件物件被初始化為無信號的,則將fInitial參數設定為FALSE。稍後,我將簡短地描述fManual參數。

要設立一個現存的事件物件,呼叫

要重置一個事件物件,呼叫

一個程式通常呼叫:

並且將第二個參數設定為INFINITE。如果事件物件目前是被設立的,那麼函式將立即傳回,否則,函式將暫停執行緒直到事件物件被設立。如果您將第二個參數設定為一個以毫秒計的超時時間值,這樣函式也可能在事件物件被設立之前傳回。

如果最初的CreateEvent呼叫的fManual參數被設定為FALSE,那麼事件物件將在WaitForSingleObject函式傳回時自動重置。這種功能特性通常使得事件物件沒有必要使用ResetEvent函式。

現在,我們可以來看一看程式20-5所示的BIGJOB2.C程式。

hEvent = CreateEvent (&sa, fManual, fInitial, pszName) ;

SetEvent (hEvent) ;

ResetEvent (hEvent) ;

WaitForSingleObject (hEvent, dwTimeOut) ;

程式20-5 BIGJOB2 BIGJOB2.C /*---------------------------------------------------------------------------- BIGJOB2.C -- Multithreading Demo (c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> #define REP 1000000 #define STATUS_READY 0 #define STATUS_WORKING 1 #define STATUS_DONE 2 #define WM_CALC_DONE (WM_USER + 0) #define WM_CALC_ABORTED (WM_USER + 1) typedef struct { HWND hwnd ; HANDLE hEvent ; BOOL bContinue ; } PARAMS, *PPARAMS ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BigJob2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Multithreading Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void Thread (PVOID pvoid) { double A = 1.0 ; INT i ; LONG lTime ; volatile PPARAMS pparams ; pparams = (PPARAMS) pvoid ; while (TRUE) { WaitForSingleObject (pparams->hEvent, INFINITE) ; lTime = GetCurrentTime () ; for (i = 0 ; i < REP && pparams->bContinue ; i++) A = tan (atan (exp (log (sqrt (A * A))))) + 1.0 ; if (i == REP) { lTime = GetCurrentTime () - lTime ; PostMessage (pparams->hwnd, WM_CALC_DONE, 0, lTime) ; } else PostMessage (pparams->hwnd, WM_CALC_ABORTED, 0, 0) ; } } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HANDLE hEvent ; static INT iStatus ; static LONG lTime ; static PARAMS params ; static TCHAR * szMessage[] = { TEXT ("Ready (left mouse button begins)"), TEXT ("Working (right mouse button ends)"), TEXT ("%d repetitions in %ld msec") } ; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; TCHAR szBuffer[64] ; switch (message) { case WM_CREATE: hEvent = CreateEvent (NULL, FALSE, FALSE, NULL) ; params.hwnd = hwnd ; params.hEvent = hEvent ; params.bContinue = FALSE ; _beginthread (Thread, 0, ¶ms) ; return 0 ; case WM_LBUTTONDOWN: if (iStatus == STATUS_WORKING) { MessageBeep (0) ; return 0 ; } iStatus = STATUS_WORKING ; params.bContinue = TRUE ; SetEvent (hEvent) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_RBUTTONDOWN: params.bContinue = FALSE ; return 0 ; case WM_CALC_DONE: lTime = lParam ; iStatus = STATUS_DONE ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_CALC_ABORTED: iStatus = STATUS_READY ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; wsprintf ( szBuffer, szMessage[iStatus], REP, lTime) ; DrawText ( hdc, szBuffer, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

處理WM_CREATE訊息時,視窗訊息處理程式首先建立一個初始化為沒信號的自動重置事件物件,然後建立執行緒。

Thread函式進入一個無限的while迴圈,在迴圈開始時首先呼叫WaitForSingleObject(注意PARAMS結構包括一個包含事件物件代號的欄位)。因為事件被初始化為重置的,所以執行緒的執行被阻擋在函式呼叫中。按下滑鼠左鍵將導致視窗程序呼叫SetEvent,這將釋放由WaitForSingleObject呼叫產生的第二個執行緒,並開始暴力測試計算。當計算完之後,執行緒再次呼叫WaitForSingleObject,但是由於第一次呼叫已經使事件物件重置,因此,執行緒將被暫停,直到再次按下滑鼠。

在其他方面,程式幾乎和BIGJOB1完全一樣。

執行緒區域儲存空間(TLS)

多執行緒程式中的整體變數(以及任何被配置的記憶體)被程式中的所有執行緒共用。在一個函式中的局部靜態變數也被使用函式的所有執行緒共用。一個函式中的局部動態變數是唯一於各個執行緒的,因為它們被儲存在堆疊上,而每個執行緒有它自己的堆疊。

對各個執行緒唯一的持續性儲存空間有存在的必要。例如,我在本章前面提到過的C中的strtok函式要求這種型態的儲存空間。不幸的是,C語言不支援這類儲存空間。但是Windows中提供了四個函式,它們實作了一種技術來做到這一點,並且Microsoft對C的擴充語法也支援它,這就叫做執行緒區域儲存空間。

下面是API工作的方法:

首先,定義一個包含需要唯一於執行緒的所有資料的結構,例如:

typedef struct { int a ; int b ; } DATA, * PDATA ;

主執行緒呼叫TlsAlloc獲得一個索引值:

這個值可以儲存在一個整體變數中或者通過參數結構傳遞給執行緒函式。

執行緒函式首先為該資料結構配置記憶體,並使用上面所獲得的索引值呼叫TlsSetValue:

該函式將一個指標和某個執行緒及某個執行緒索引相關聯。現在,任何需要使用這個指標的函式(包括最初的執行緒函式本身)都可以包含如下所示的程式碼:

dwTlsIndex = TlsAlloc () ;

TlsSetValue (dwTlsIndex, GlobalAlloc (GPTR, sizeof (DATA)) ;

PDATA pdata ; ... pdata = (PDATA) TlsGetValue (dwTlsIndex) ;

現在函式可以設定或者使用pdata->a和pdata->b了。在執行緒函式終止以前,它釋放配置的記憶體:

當使用該資料的所有執行緒都終止之時,主執行緒將釋放索引:

這個程序剛開始可能令人有些迷惑,因此如果能看一看如何實作執行緒區域儲存空間可能會有幫助(我不知道Windows實際上是如何實作的,但下面的方案是可能的)。首先,TlsAlloc可能只是配置一塊記憶體(長度為0)並傳回一個索引值,即指向這塊記憶體的一個指標。每次使用該索引呼叫TlsSetValue時,通過重新配置將記憶體塊增大8個位元組。在這8個位元組中儲存的是呼叫函式的執行緒ID(通過GetCurrentThreadId來獲得)以及傳遞給TlsSetValue函式的指標。TlsSetValue簡單地使用執行緒ID來搜尋作業系統管理的執行緒區域儲存空間位址表,然後傳回指標。TlsFree將釋放記憶體塊。所以您看,這可能是一件容易得可以由您自己來實作的事情。不過,既然已經有工具為您做好了這些工作,那也不錯。

Microsoft對C的擴充功能使這件工作更加容易。只要在要對每個執行緒都保留不同內容的變數前加上__declspec (thread)就好了。對於任何函式的外部靜態變數,則為:

對於函式內部的靜態變數,則為:

GlobalFree (TlsGetValue (dwTlsIndex)) ;

TlsFree (dwTlsIndex) ;

__declspec (thread) int iGlobal = 1 ;

__declspec (thread) static int iLocal = 2 ;