建立新的線程的API函數是CreateThread,它的語法如下:
hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,
pParam, dwFlags, &idThread) ;
第一個參數是指向SECURITY_ATTRIBUTES型態的結構的指針。在Windows 98中忽略該參數。在Windows NT中,它被設爲NULL。第二個參數是用於新線程的初始堆棧大小,默認值爲0。在任何情況下,Windows根據需要動態延長堆棧的大小。
CreateThread的第三個參數是指向線程函數的指標。函數名稱沒有限制,但是必須以下列形式聲明:
DWORD WINAPI ThreadProc (PVOID pParam) ;
CreateThread的第四個參數爲傳遞給ThreadProc的參數。這樣主線程和從屬線程就可以共享數據。
CreateThread的第五個參數通常爲0,但當建立的線程不馬上執行時爲旗標CREATE_SUSPENDED。線程將暫停直到呼叫ResumeThread來恢復線程的執行爲止。第六個參數是一個指標,指向接受執行緒ID值的變量。
大多數Windows程序寫作者喜歡用在PROCESS.H表頭文件中聲明的C執行時期鏈接庫函數_beginthread。它的語法如下:
hThread = _beginthread (ThreadProc, uiStackSize, pParam) ;
它更簡單,對於大多數應用程序很完美,這個線程函數的語法爲:
void __cdecl ThreadProc (void * pParam) ;
再論隨機矩形
程序20-1 RNDRCTMT是第五章裏的RANDRECT程序的多線程版本,您將回憶起RANDRECT使用的是PeekMessage循環來顯示一系列的隨機矩形。
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支持-都是相同的。
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) ;
}