20.4 触发事件

摘录于《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 是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值