// 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, 秏s) ;
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, 秏s) ;
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應該沒有問題。