摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P947
多线程通常被用于需要长时间运行任务的程序。我们把这种任务称为“大任务”,也就是运行时间超过 1/10 秒的任务。常见的例子有 Word 里的拼写检查、数据库中的文件排序或索引、电子表格的重新计算、打印、复杂的绘制。当然,目前防止违反 1/10 秒规定的最好方法是使用二级线程来运行大任务。这些二级线程不创建窗口,所以不被 1/10 秒的规定约束。
当二级线程结束后,它们需要通知主线程。或者,有时主线程也需要通知二级线程终止运行。下面的内容展示如何实现主线程和二级线程的交互。
20.4.1 BIGJOB1 程序
我使用了一系列浮点计算——有时又称为“savage”(中文含义为野蛮)基准——来作为假设的大任务。这个计算以如下方式不断增加正数的值:对数字取平方,再取平方根(这取消平方的作用),取对数和指数(也是互相取消),取反切和正切函数(再次互相取消),最后加 1 得到结果。
BIGJOB1 程序如图 20-4 所示。
/*-----------------------------------------------
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 hPrecInstance,
PSTR 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);
}
a
这个程序很简单,但你可以看到如何在多线程程序中处理大任务。你可以通过在窗口的客户区单击鼠标运行 BIGJOB1 程序。该程序进行一百万次上述的计算。在 300 MHz 的 Pentium II 机器上,大概需要两秒。当运算结束,所用的时间被显示在窗口中。在运算过程中,你可以在窗口客户区右击鼠标来终止它。
让我们来看一下它的实现。
窗口过程有一个叫 iStatus 的静态变量(它可以被设置为程序开始定义的三个以 STATUS 为前缀的常量之一),用于指示程序是否可以开始运算,还是正在运算中,或是运算已结束。程序根据 iStatus 的值在处理 WM_PAINT 消息时在客户区中间显示适当的字符串。
窗口过程还定义了一个静态结构(类型为 PARAMS,这个类型也在程序的开始处定义)用来在窗口过程和二级线程之间共享数据。这个结构只有两个字段——hwnd(程序窗口的句柄)和 bContinue。后者是一个布尔变量,用来指示线程是否应该继续运算。
当你在客户区单击鼠标时,窗口过程把 iStatus 设置为 STATUS_WORKING 并初始化 PARAMS 的两个字段。hwnd 字段被设置为窗口的句柄。bContinue 被设置为 TRUE。
接着窗口过程调用 _beginthread 函数。被称为 Thread 的二级线程函数首先调用 GetCurrentTime 函数获取以毫秒为单位的当前时间。然后,它开始循环一百万次计算。注意,如果 bContinue 被设置为 FALSE,线程会退出循环。
完成 for 循环后,线程函数检查是否完成了一百万次的计算。如果是,那么它再次调用 GetCurrentTime 函数来计算经过的时间,并给窗口过程发送一个由程序定义的 WM_USER_DONE 消息,其中经过的时间用 lParam 参数传递。如果计算提前终止(也就是说,PARAMS 的 bContinue 字段在循环中变成 FALSE),线程则给窗口过程发送 WM_USER_ABORTED 消息。之后二级线程调用 _endthread 退出。
在窗口过程内,PARAMS 结构的 bContinue 字段在你在客户区右击鼠标时被设置为 FALSE,这使得计算在完成前被终止。
值得注意的是,线程中的 pparams 变量被定义为 volatile 类型。这个类型标识符告诉编译器,该变量会在正常执行之外被修改(比如通过另外一个线程)。否则,编译器优化可能会跳过在每个循环中检查 bContinue 的代码,因为它在循环内没有被修改。volatile 这个关键字能防止这种优化。
窗口过程在处理 WM_USER_DONE 消息时,首先保存程序花费的时间。对 WM_USER_DONE 和 WM_USER_ABORTED 消息的处理都会接着调用 InvalidateRect 来产生一个 WM_PAINT 消息,使得一个新字符串在客户区显示。
通常情况下,最好能实现一个能让线程正常退出的方法。比如,利用结构中的 bContinue 字段。KillThread 函数只能在正常退出没法实现时才使用。这主要是为了防止资源(比如分配的内存)无法释放,如果内存在线程终止时没有被释放,它就会一直占据那块内存。线程不是进程:因为同一进程里分配的资源被所有线程共享,所以线程退出时资源不会被自动释放。好的程序设计会要求线程在结束时释放由它分配的资源。
另外在下面的情况中,第三个线程可能会在第二个线程仍在运行时被创立:Windows 在 SendMessage 调用和 _endthread 调用之间从第二个线程切换到第一个线程,然后窗口过程因为响应鼠标输入而创立了一个新的线程。在这里,这种情况不会带来什么问题。但如果你不希望在你的程序中遇到这个问题,那么可以使用临界区来避免线程冲突。
20.4.2 事件对象
程序 BIGJOB1 在每次执行蛮力计算的时候都创建一个新线程,并在计算结束的时候终止这个线程。显而易见,我们需要一种更有效的线程使用方式。
一种思路是在程序的整个生命期中只创建并保持一个计算线程,并仅在需要的时候才调用这个线程。这正是事件对象的用武之地。
一个事件对象有两种状态:已被触发(也称为设置)或未被触发(也称为复位)。下面的代码创建一个事件对象:
hEvent = CreateEvent (&sa, fManual, fInitial, pszName);
函数的第一个参数(一个指向 SECURITY_ATTRIBUTES 结构的指针)和最后一个参数(事件对象名)只在事件对象在进程间共享时才有意义。在一个单进程程序中,这些参数通常被设置为 NULL。参数 fInitial 指示事件 的初始触发状态:为 TRUE 表示事件对象初始为已被触发;为 FALSE 表示初始未被触发。参数 fManual 将在后面介绍。
为了触发一个已有的事件对象,我们可以调用:
SetEvent (hEvent);
为了解除一个事件对象的触发状态,我们可以调用:
ResetEvent (hEvent);
一个程序调用下面的函数来等待一个事件对象被触发:
WaitForSingleObject (hEvent, dwTimeOut);
如果事件对象已经处于触发状态,函数就立刻返回;否则,函数就会等待 dwTimeOut 毫秒。如果 dwTimeOut 被设置为 INFINITE,函数就会一直等到事件对象被触发才返回。
如果最开始的 CreateEvent 函数的 fManual 参数被设置为 FALSE,那么函数 WaitForSingleObject 返回后,事件对象的状态就会被自动设置为未触发。这样,我们就不需要调用 ResetEvent 函数去重置事件对象状态。
现在,我们可以来研究图 20-5 中的 BIGJOB2 程序了。
/*-----------------------------------------------
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 APIENTRY WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrecInstance,
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.hwnd = hwnd;
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 的处理中,我们先创建一个事件对象,设置其 触发方式为自动触发(fManual 为 FALSE),初始状态为未触发。然后我们创建一个线程。
Thread 函数包含一个无限的 while 循环,并在每个循环开始的时候调用函数 WaitForSingleObject。(注意,PARAMS 结构的第三个参数是事件对象的句柄。)因为事件的初始状态是未触发,这个线程会在函数调用时挂起。当我们单击鼠标左键,函数 SetEvent 被调用,从而解除了 WaitForSingleObject 造成的二级线程的等待,于是该线程开始进行蛮力计算。因为事件对象是自动复位的,因此当计算结束,线程再次调用 WaitForSingleObject 的时候,这个线程又将被挂起,等待下一次鼠标单击来触发事件。
程序的其他部分和 BIGJOB1 是一样的。