摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P929
创建一个执行线程的 API 函数叫 CreateThread,它的语法如下:
hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,
pParam, dwFlags, &idThread);
第一个参数是一个指向 SECURITY_ATTRIBUTES 结构的指针。这个参数在 Windos 98 中没有用到;在 Windows NT 中,你也可以设置其为 NULL。第二个参数表示线程栈的初始大小,可以被设置为 0 表示取默认值。
不管哪种情况,Windows 都会根据需要动态调整线程栈的大小。
第三个参数是一个函数指针,指向你的线程函数。这个函数可以任意命名,但必须有如下的格式:
DWORD WINAPI ThreadProc (PVOID pParam);
CreateaThread 的第四个参数就是传递给 ThreadProc 的参数,利用这个参数,我们可以在主线程和二级线程间共享数据。
CreateThread 的第五个参数通常被设置为 0。如果被设置为 CREATE_SUSPENDED,就表示这个线程在创建后不被立即执行。这个线程将保持在挂起状态,一直到我们调用 ResumeThread 函数为止。第六个参数是一个指针,指向接收新线程的 ID 值的变量。
大部分的 Windows 程序员倾向于用 C 的运行时库函数 _beginthread(在头文件 PROCESS.H 中定义)。这个函数的语法如下:
hThread = _beginthread(ThreadProc, uiStackSize, pParam);
这个函数的使用方式更加简单,而它对大部分的应用程序都够用了。它的线程函数格式如下:
void __cdecl ThreadProc (void * pParam);
20.2.1 随机矩形程序的多线程版本
图 20-1 显示了随机矩形程序 RNDRCTMT(在第 5 章中介绍过)的多线程版本。在前面的的章节中,RNDRCTMT 用了一个基于 PeekMessage 的循环来显示一系列随机产生的矩形。
/*-----------------------------------------------
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 hPrecInstance,
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);
}
当你创建一个多线程程序的时候,需要更改一些 Project Settings 对话框里面的配置。从 C/C++ 选项卡中,选择 Category 组合框里的 Code Generation。在 Use Run-Time Libaray 组合框中,你可以看到设置单线程模式的发布配置和调试单线程模式的调试配置,你需要把它们都改成多线程模式。这样做会给编译器加一个/MT 标志,让它编译多线程应用程序:OBJ 文件中的 LIBC.LIB 被 LIBCMT.LIB 取代。链接器根据这个名字链接相应的运行时库函数。
LIBC.LIB 和 LIBCMT.LIB 都提供 C 语言的基本库函数。某些 C 的库函数会维护一些静态数据,比如 strtok 函数,它可以被连续调用并会在静态内存中储存一个指针。在一个多线程程序中,每个线程在 strtok 函数中都必须有自己的静态指针。因此多线程版本的 strtok 和单线程版本中的实现是不一样的。
此外,我还在 RNDRCTMT.C 中添加了头文件 PROCESS.H。这个头文件定义了用以启动新线程的函数 _beginthread。这个函数只有在 _MT 编译选项下才会被定义,而这也是/MT 标志的另一个作用。
在 RNDRCTMT.C 的 WinMain 函数里,CreateWindow 函数返回的句柄 hwnd 被存储在一个全局变量里,从窗口过程的 WM_SIZE 消息中获取的 cxClient 和 cyClient 值也被存储在全局变量中。
RNDRCTMT 程序的窗口过程调用 _beginthread 的方式很简单,只是把线程函数(名为 Thread)的地址传递给 _beginthread 作为其第一个参数,其他的参数都设置为 0。我们的线程函数返回类型是 VOID,参数也是一个 VOID 类型的指针。我们的线程函数不用这个指针传递数据。
在调用 _beginthread 函数后,此线程函数中的代码和其他会被此线程调用的函数,会跟程序的其他代码同时运行。两个或多个线程可以使用一个进程中的相同的函数。在上面的例子中,自动局部变量(存储在栈上)是每个线程私有的数据,而静态变量是所有线程间共享的。因此,我们可以在窗口过程中设定全局的 cxClient 和 cyClient 变量,而 Thread 函数可以访问这两个值。
有的时候,你可能需要线程拥有私有的持久数据。通常情况下,持久数据会涉及静态变量的使用,但在 Windows 98 中,你可以使用我提过的 TLS,而且我们会在本章后面详细介绍 TLS 的使用。
20.2.2 编程竞赛问题
1986 年 10 月 3 日,微软举办了一个一天的座谈会,向计算机杂志的技术编辑和作者介绍公司的语言开发工具,包括其第一次推出的交互式开发环境 QuickBASIC 2.0。当时,Windows 1.0 刚刚推出不到一年,而大家还都没有意识到微软已经有这样的产品。(他们花了几年的时间才让这些产品的概念广为人知。)这次座谈会与众不同的地方在于,公司的公共关系部门搞了一个特别的编程竞赛——“挑战盖茨”。比尔●盖茨将用 QuickBASIC 2.0 作为编程工具,而出版界的人士可以选用任何他们想用的工具。
为了公平,编程的题目从竞赛参与者提出的几个题目中随机抽取。大家被要求用半个小时写出这个程序,让我们看看最后选了什么。
创建一个有四个窗口的多线程仿真程序。第一个窗口显示一个递增的数列,第二个窗口显示一个递增的质数数列,第三个窗口显示一个递增的斐波那契数列。(斐波那契数列的第一个第二个元素是 0 和 1,以后的元素由前面相邻的两个元素相加得到:0, 1, 1, 2, 3, 5, 8, 以此类推。)当数列显示到窗口底部的时候,这三个窗口应该显示滚动条或者清除原有的显示内容以便于继续显示新的数据。第四个窗口显示一些随机产生的大小不一的圆。按 Esc 键可以退出整个程序。
当时(1986 年的 10 月),在 DOS 底下写一个真的多线程程序是不可能的,没有一个参赛者有这样的自信,而且大部分人对 Window 开发没有什么经验。即使是有经验的,要从头写这样一个程序也不可能在半小时中完成。
大部分参赛者的程序把屏幕分成 4 个部分,然后用一个循环来顺序更新每个窗口的内容并检查 Esc 键是否被按下。因为是 DOS 的程序,它们都占用了 100% 的 CPU 时间。
下面的 MULTI1 例程就是针对这个题目的实现。这个版本和 Windows 1.0 下面的实现很类似(主要的区别在于新的版本是 32 位的),除了变量和函数参数的定义以及 Unicode 支持外,大部分的代码和程序结构是一样的。
/*-----------------------------------------------
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 hPrecInstance,
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, 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((double)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, 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, TEXT("%d"), iNum));
ReleaseDC(hwnd, hdc);
iTemp = iNum;
iNum = iNext;
iNext += 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 CALLBACK 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 = LoadIcon(NULL, IDI_APPLICATION);
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);
}
这个程序里面没有什么新鲜东西。主窗口创建了四个子窗口,每个都占据了四分之一的客户区。主窗口设置了一个计时器,用于向每个子窗口发送 WM_TIMER 消息。
通常,在 WM_PAINT 消息处理时,Windows 程序需要维护足够的信息用来重新绘制一个窗口的内容。MULTI1 没有这样做,因为窗口更新得很快,所以我们没有必要维护以前的内容。
WndProc2 中的质数产生器用了一个效率很低的方法:检查一个数是否能被 1 和自己以外的另一个整数整除来判断这个数是否为质数。我们不需要检查所有比这个数小的整数,而只需检查到这个数的平方根就可以了,这样可以减少很多的计算量。在平方根的计算中,我们用了浮点数据类型,这在这个基于整数的程序中略现突兀。
MULTI1 程序使用了计时器来完成一个多线程的任务,这在早期(和当前)的 Windows 版本中没有什么问题。但是使用计时器有时会人为地限制了一个程序的执行速度。如果程序能在一条 WM_TIMER 消息中处理完所有的任务并有空余,就意味着我们没有充分利用 CPU 的计算能力。
一个可能的解决方案是在一个 WM_TIMER 消息中处理多个更新,但这样做的话就会面临一个新问题:计算多个更新的话就需要根据计算机的速度来适当调整。而计算机的速度变得太快了:25 MHz 的 386,50Mhz 的 486,还有 1GHz 的奔腾,没有人愿意根据计算机速度而写程序。
20.2.3 多线程的解决方案
让我们看一看这个问题的多线程解决方案。图 20-3 显示了 MULTI2 程序。
/*-----------------------------------------------
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 hPrecInstance,
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((double)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 CALLBACK 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 = LoadIcon(NULL, IDI_APPLICATION);
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:
KillTimer(hwnd, 1);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
a
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 函数的新线程并且传递给新线程一个指向该 PARAMS 结构的指针。在处理 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 中,输出会继续。
20.2.4 还有问题吗?
看上去,MULTI2 并不是完美无瑕。为什么这么说呢?让我们以 MULTI2.C 中 WndProc1 和 Thread1 为例来看看多线程的问题。
WndProc1 在 MULTI2 中作为主线程运行,Thread1 和它并行运行。Windows 98 何时在这两个线程之间进行切换是随时变化而且不可预知的。假设 Thread1 在运行,并刚执行了查看 PARAMS 结构的 bKill 字段是否是 TRUE 的代码。假设在这时,bKill 不是 TRUE。但是随后 Windows 98 切换控制到主线程。在这时,用户终止了程序。WndProc1 收到 WM_DESTROY 消息,把 bKill 设定为 TRUE。可是,太晚了。操作系统突然又切换到 Thread1,而这个函数会试图获取一个不存在的窗口的设备环境句柄。
实际上,Windows 98 本身对此有足够的保护。图形函数会返回失败但并不引起任何问题。
正确的多线程编程要求使用线程同步(特别是临界区),我会在下面详细讨论。基本上,临界区是通过调用 EnterCriticalSection 和 LeaveCriticalSection 来定义的。如果一个线程进入了临界区,另外一个线程就不能进入。后者会被阻塞(block)在 EnterCriticalSection 函数调用,知道前一个线程调用 LeaveCriticalSection。
MULTI2 的另外一个问题是主线程可能收到 WM_ERASEBKGND 或 WM_PAINT 消息,而同时二级线程正在输出绘图。使用临界区可以防止两个线程同时在一个窗口上绘制。但实验显示 Windows 98 会串行化对图形绘制函数的使用。也就是说,当一个线程在一个窗口绘图时,另外一个线程不可能在这个窗口上同时绘图。
Windows 98 文档指出有一类图形函数是非串行化的。这包括使用的 GDI 对象,比如画笔、画刷、字体、位图、区域和调色板。有可能出现一个线程销毁了另一个线程正在使用的对象的情况。使用临界区是一个解决方案。另一个更好的方法是不要在线程间共享 GDI 对象。
20.2.5 休眠的好处
前面讨论过,我认为多线程最好的架构是让主线程创立所有的窗口,拥有所有窗口过程并处理所有窗口消息,二级线程用于处理后台或长时间运行的任务。
但是,假定你要在二级线程里制作动画。通常,在 Windows 中,动画是通过处理 WM_TIMER 消息实现的。但是,如果二级线程没有创立一个窗口,它就不能接收这些消息。而没有时间控制,动画会运行得过快。
这个问题可以通过 Sleep 函数解决。实际上,一个线程通过调用 Sleep 函数来主动暂停执行。该函数的唯一参数是以毫秒为单位的时间。Sleep 函数知道经过给定的时间才会返回。在这段时间里,这个线程被挂起,不会分配时间片(当然,该线程在系统处理计时器滴答时仍需要很少的时间来检查是否应该继续执行该线程)。传入参数 0 给 Sleep 函数使得线程的现有时间段作废。
当一个线程调用 Sleep 时,只有该线程会被系统挂起。系统仍会运行同一进程中或其他进程中的其他线程。我在第 14 章的 SCRAMBLE 程序中使用了 Sleep 来减慢打乱操作。
通常情况下,你不应该在主线程中使用 Sleep 函数,因为这会减慢消息的处理。由于 SCRAMBLE 程序不创立窗口,所以在其中使用 Sleep 函数不会有问题。