Windows系统中每个窗口进程都有一个独立的消息队列。这个实现看起来很好:如果一个程序在花很长时间处理一条消息,那么当鼠标在该程序的窗口上时,鼠标指针会显示为沙漏,而当它在另一个程序的窗口上时,就变成正常的箭头了。简单的单击就能把另外的这个窗口调到前台。
然而,用户仍然不能在运行这个大任务的窗口工作,因为这个大任务阻止了程序接收其他消息。这不是我们想要的。一个程序应该总是能接收消息,而这常常就需要用到二级线程。
在Windows NT和Windows 98中,没有消息队列线程和非消息队列线程的区分。每个 线程在被生成时都有它自己的消息队列。在绝大多数情况下,你应该在主线程中通过消息过程处理输入,而把长时间运行的任务转给其他不带窗口的线程。我们很快就会看到,这种结构几乎总是最合理的。本节我们将讲述Windows多线程。
本节必须掌握的知识点:
多线程结构
线程的麻烦
与线程相关的函数
第147练:多线程画随机矩形
第148练:使用计时器模拟多线程
第149练:多线程解决方案
19.2.1 多线程结构
建议程序架构应该像这样:主线程创建程序所需要的所有窗口,包括这些窗口的所有窗口过程,并处理这些窗口的消息。任何其他的线程都应该是简单的后台运算。除了通过与主线程通信外,它们不和用户打交道。
主线程处理用户输入(和其他消息),在此过程中或许会生成其他的二级线程。这些额外的线程处理跟用户不相关的任务。
●处理用户界面的线程
这些线程创建窗口并设置消息循环来负责处理窗口消息,一个进程中并不需要太多这种线程,一般让主线程负责这个工作就可以了。同时,这种线程不该处理1/10秒以上的工作。
●工作线程
该类线程不处理窗口界面,当然也就不用处理消息了,它一般在后台运行,干一些繁重的、需要长时间运行的粗活,一般都要遵循1/10秒规则,但工作线程的要求相对比较宽松。
19.2.2 线程的麻烦
要正确地设计、编码和调试一个复杂的多线程应用程序,显然是一个Windows程序员会碰到的最困难的工作之一。因为一个抢占式多任务系统能够在任一点中断一个线程,把控制切换到另一个线程,因此这两个线程之间的任何不正常交互可能并不明显,而且可能只是偶尔才出现,看起来更像是随机的。
多线程程序中有两个常见的缺陷:
●竞争条件:不同的线程要访问同一资源,就不可避免会出现“竞争”,而问题解决的结果依赖于线程运行的时序,而不是程序的逻辑,这种情况称为竞争条件
●“死锁”:当两个程序己经中断彼此的运行,但是它们只有继续进行才能解除对方被中断的运行。
图19-1 死锁
如图19-1所示,T1线程请求使用R2资源,但此时R2资源正在被T2线程占用,因此T1线程只能挂起等待。当T2线程请求使用R1资源时,R1资源正在被T1线程所占用。因为此时T1线程处于挂起状态,无法及时释放R1资源,因此T2线程只能挂起等待。此时,演变为T1线程和T2线程相互等待对方释放资源R1和R2,这种情形称为“死锁”。这是所有程序员在处理多线程时都必须极力避免出现的情形。
19.2.3与线程相关的函数
■创建线程
CreateThread函数用于创建一个新的线程。它可以在一个进程内创建一个新的执行流,使得多个线程可以并发执行。
函数原型如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全属性,默认为NULL
SIZE_T dwStackSize, // 线程堆栈大小(以字节为单位),0为默认堆栈大小
LPTHREAD_START_ROUTINE lpStartAddress, // 线程入口点函数
LPVOID lpParameter, // 传递给线程函数的参数
DWORD dwCreationFlags, // 线程创建标志,用于控制线程的创建方式
LPDWORD lpThreadId // 接收线程标识符的变量,用于标识新线程
);
函数返回值为新线程的句柄(HANDLE 类型),如果创建线程失败,则返回 NULL。
使用 CreateThread 函数可以创建一个新的线程,并指定线程函数来执行特定的任务。可以通过设置线程的入口点函数和参数来定义线程的行为。创建后的线程可以与其他线程并发执行,实现多任务处理。
【注意】在使用 CreateThread 函数创建线程后,需要使用 CloseHandle 函数关闭线程句柄,以确保资源的正确释放。
以下是一个简单的示例代码,演示如何使用 CreateThread 函数创建一个新线程:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
printf("Thread function is executing.\n");
return 0;
}
int main() {
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, &dwThreadId);
if (hThread == NULL) {
printf("Failed to create thread.\n");
return 1;
}
// 等待线程结束
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
以上代码中,ThreadFunction 是线程的入口点函数,在新线程中执行。CreateThread 函数创建一个新线程,并将 ThreadFunction 指定为线程的入口点。主线程使用 WaitForSingleObject 函数等待新线程的结束,最后使用 CloseHandle 函数关闭线程句柄。
C标准库函数:_beginthread
_beginthread是Windows平台上的一个C/C++运行时库函数,用于创建一个新线程并开始执行指定的线程函数。
以下是_beginthread函数的原型和用法:
uintptr_t _beginthread(
void (*start_address)(void*), //指向线程函数的指针
unsigned stack_size, // 线程的栈大小(字节为单位)0表示使用默认的栈大小
void* arglist //传递给线程函数的参数指针
);
_beginthread函数会创建一个新线程,并开始执行指定的线程函数。线程函数的参数可以通过arglist传递给线程函数。
以下是一个简单示例,演示如何使用_beginthread函数创建和启动一个新线程:
#include <process.h>
#include <stdio.h>
void ThreadFunction(void* arg) {
// 线程任务...
printf("Thread completed.\n");
}
int main() {
uintptr_t threadHandle;
threadHandle = _beginthread(ThreadFunction, 0, NULL);
if (threadHandle == -1) {
printf("Failed to create thread.\n");
return 1;
}
// 等待线程结束
// ...
return 0;
}
在上述示例中,我们使用_beginthread函数创建了一个新线程,并指定了一个线程函数ThreadFunction。在主函数中,我们可以执行一些操作来等待线程结束,例如使用WaitForSingleObject函数等待线程的句柄。
■线程函数
线程函数(Thread Function)是在线程中执行的代码块或函数,它定义了线程的具体行为和任务。线程函数是通过线程的入口点来调用的,当线程被创建并启动后,它会从线程函数的起始处开始执行。
DWORD WINAPI ThreadProc(LPVOID lpParameter);
C语言中线程函数的定义和使用参见创建线程函数CreateThread示例代码。
■终止线程
在Windows操作系统中,可以使用TerminateThread函数来终止一个线程。该函数会立即终止目标线程的执行,不会给线程执行任何清理的机会,因此使用该函数需要谨慎。
以下是TerminateThread函数的原型:
BOOL TerminateThread(
HANDLE hThread, //目标线程的句柄
DWORD dwExitCode //线程的退出代码,表示线程被终止的原因
);
函数返回值为 BOOL 类型,表示函数执行的成功与否。如果函数成功执行,返回值为非零值;如果函数执行失败,返回值为零。
【注意】终止线程可能导致一些不可预测的结果和资源泄漏。因此,建议在正常情况下使用线程间的协作机制来安全地结束线程,如使用共享变量、事件、条件变量等。
以下是一个示例代码,演示如何使用TerminateThread函数终止一个线程:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
while (1) {
// 线程任务...
}
return 0;
}
int main() {
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, &dwThreadId);
if (hThread == NULL) {
printf("Failed to create thread.\n");
return 1;
}
// 等待一段时间,让线程执行
Sleep(2000);
// 终止线程
if (TerminateThread(hThread, 0)) {
printf("Thread terminated.\n");
} else {
printf("Failed to terminate thread.\n");
}
CloseHandle(hThread);
return 0;
}
在上述示例中,我们创建了一个线程,并在线程函数中使用一个无限循环来模拟线程的执行。在主函数中,等待线程执行了一段时间后,调用TerminateThread函数来终止线程。然后,通过判断函数的返回值来确定是否成功终止线程。
安全地结束一个线程的执行是一个重要的任务,可以通过以下几种方式来实现:
●协作标志:使用一个共享的标志或变量,线程在执行过程中检查该标志或变量的状态,当标志或变量满足某个条件时,线程自行退出。这种方式需要线程函数周期性地检查标志或变量,以决定是否退出。
●通知事件:使用事件对象(如CreateEvent函数创建的事件)进行线程间的通信。主线程或其他线程可以通过设置事件来通知目标线程退出。线程在执行过程中等待事件的信号,当收到信号时,线程退出。
●取消标志:使用取消标志来通知线程退出。线程在执行过程中周期性地检查取消标志,当标志被设置时,线程自行退出。这种方式需要线程函数的设计支持,合适的时机检查取消标志。
●线程间消息传递:使用消息队列或消息机制,主线程或其他线程发送特定消息给目标线程,通知它退出。线程在执行过程中检查消息队列,当收到退出消息时,线程退出。
●清理函数:在线程函数中使用清理函数(cleanup function)来释放资源和执行必要的清理操作。主线程或其他线程可以通过设置标志来通知目标线程执行清理函数,并安全地终止线程。
需要注意的是,以上方法都需要在线程函数中进行适当的处理,确保在退出之前释放资源、关闭句柄等操作,以避免资源泄漏和不一致的状态。
C/C++语言中的线程退出函数:
pthread_exit():在POSIX标准的线程库中,用于退出当前线程,并返回一个退出码。
CreateThread API函数创建线程,ExitThread API函数销毁线程。
_beginthread:创建线程,_endthread来销毁线程
exit():在C标准库中,用于终止整个进程,包括所有线程。
return语句:在线程函数中使用return语句来退出线程。
●ExitThread函数
ExitThread函数会立即终止当前线程的执行,不会执行后续的代码。线程终止后,它的资源会被自动清理,包括栈、句柄等。
【注意】ExitThread函数只能用于退出当前线程,不能用于终止其他线程。如果要终止其他线程,可以使用TerminateThread函数,但请谨慎使用,因为它可能会导致资源泄漏和不一致的状态。
以下是一个简单示例,演示如何使用ExitThread函数退出线程:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
// 线程任务...
// 退出线程
DWORD exitCode = 123;
ExitThread(exitCode);
}
●_endthread函数
_endthread函数是一个在Windows平台上的C/C++运行时库函数,用于终止线程的执行。它与ExitThread函数类似,都是用于退出线程的函数,但在使用上有一些差异。
以下是_endthread函数的原型和用法:
void _endthread(void);
_endthread函数会终止当前线程的执行,并自动清理线程相关的资源,包括栈空间等。与ExitThread函数不同,_endthread函数不需要传递退出码,它会自动使用默认的退出码。
【注意】_endthread函数只能用于退出当前线程,不能用于终止其他线程。
以下是一个简单示例,演示如何使用_endthread函数退出线程:
#include <process.h>
#include <stdio.h>
void ThreadFunction(void* arg) {
// 线程任务...
// 退出线程
_endthread();
}
●当线程终止运行时
线程终止运行时,会发生下面这些事情:
1.线程拥有的所有用户对象句柄会被释放。在Windows中,大多数对象都是由包含了“创建这些对象的线程”的进程拥有的。但是,一个线程有两个User对象:窗口(window)和挂钩(hook)。一个线程终止运行时,系统会自动销毁由线程创建或安装的任何窗口,并卸载由线程创建或安装的任何挂钩。其他对象只有在拥有线程的进程终止时才被销毁。
2.线程的退出代码从STILL_ACTIVE变成传给ExitThread或TerminateThread的代码。
3.线程内核对象的状态变为signaled。
4.如果线程是进程中的最后一个活动线程,系统认为进程也终止了。
5.线程内核对象的使用计数递减1。
线程终止运行时,其关联的线程对象不会自动释放,除非对这个对象的所有未结束的引用都被关闭了。
一旦线程不再运行,系统中就没有别的线程可以处理该线程的句柄。但是,其他线程可以调用GetExitCodeThread来检查hThread所标识的那个线程是否已终止运行;如果已终止运行,就判断其退出代码是什么:
BOOL GetExitCodeThread( HANDLE hThread,PDWORD pdwExitCode);
退出代码的值通过pdwExitCode指向的DWORD来返回。如果在调用GetExitCodeThread时,线程尚未终止,函数就用STILL_ACTIVE标识符(被定义为0x103)来填充DWORD。如果函数调用成功,就返回TRUE。
■获取线程退出码
GetExitCodeThread函数用于获取指定线程的退出代码。它可以查询一个线程的退出状态,以确定线程是正常退出还是异常退出。
函数原型如下:
BOOL GetExitCodeThread(
HANDLE hThread, //要查询的线程句柄
LPDWORD lpExitCode //指向 DWORD 类型变量的指针,用于接收线程的退出代码
);
函数返回值为 BOOL 类型,表示函数执行的成功与否。如果函数成功执行,返回值为非零值;如果函数执行失败,返回值为零。
GetExitCodeThread函数,线程结束后的退出码可被其他线程用GetExitCodeThread检测到。
以下是一个简单的示例代码,演示如何使用GetExitCodeThread函数获取线程的退出代码:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
Sleep(2000); // 模拟线程执行一段时间
return 123; // 设置线程的退出代码为123
}
int main() {
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, &dwThreadId);
if (hThread == NULL) {
printf("Failed to create thread.\n");
return 1;
}
// 等待线程结束
WaitForSingleObject(hThread, INFINITE);
// 获取线程的退出代码
DWORD exitCode;
if (GetExitCodeThread(hThread, &exitCode)) {
printf("Thread exit code: %d\n", exitCode);
} else {
printf("Failed to get thread exit code.\n");
}
CloseHandle(hThread);
return 0;
}
在上述示例中,ThreadFunction 是线程的入口点函数,在新线程中执行。在函数的最后,设置了线程的退出代码为123。在主线程中,我们创建一个新的线程,等待线程结束后,使用GetExitCodeThread函数获取线程的退出代码,并将其打印出来。
■挂起线程函数
SuspendThread函数用于挂起指定线程的执行。以下是SuspendThread函数的原型:
DWORD SuspendThread(
HANDLE hThread //要挂起的线程的句柄
);
函数返回值为 DWORD 类型,表示函数执行的结果。如果函数执行成功,返回值为线程的先前挂起计数;如果函数执行失败,返回值为-1。
注意事项:
●SuspendThread函数会挂起目标线程的执行,使线程进入等待状态,直到被其他线程恢复。
●需要注意线程的挂起计数。每次调用SuspendThread函数会增加线程的挂起计数,而每次调用ResumeThread函数会减少线程的挂起计数。只有当挂起计数为0时,线程才会继续执行。
●在多线程环境中使用SuspendThread函数时,需要小心处理线程同步和死锁等问题,以免导致线程间的竞争和死锁情况。
以下是一个简单的示例代码,演示如何使用SuspendThread函数挂起和恢复线程的执行:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
while (1) {
// 线程任务...
}
return 0;
}
int main() {
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, &dwThreadId);
if (hThread == NULL) {
printf("Failed to create thread.\n");
return 1;
}
// 挂起线程
DWORD suspendCount = SuspendThread(hThread);
if (suspendCount != -1) {
printf("Thread suspended. Suspend count: %lu\n", suspendCount);
} else {
printf("Failed to suspend thread.\n");
}
// 恢复线程
DWORD resumeCount = ResumeThread(hThread);
if (resumeCount != -1) {
printf("Thread resumed. Suspend count: %lu\n", resumeCount);
} else {
printf("Failed to resume thread.\n");
}
CloseHandle(hThread);
return 0;
}
在上述示例中,我们创建了一个线程,并在主函数中使用SuspendThread函数将线程挂起。然后,使用ResumeThread函数恢复线程的执行。通过分别检查挂起计数的返回值,可以确定函数是否执行成功,并获得线程的挂起计数。
【注意】使用SuspendThread函数需要小心处理线程同步和死锁等问题。在实际应用中,更推荐使用更安全和可控的线程同步机制,如事件、互斥量、条件变量等。
■恢复(唤醒)线程
ResumeThread(hThread):计数器减1,直到0时,才唤醒。
ResumeThread函数用于恢复被挂起的线程的执行。以下是ResumeThread函数的原型:
DWORD ResumeThread(
HANDLE hThread //要恢复执行的线程的句柄
);
函数返回值为 DWORD 类型,表示函数执行的结果。如果函数执行成功,返回值为线程的先前挂起计数;如果函数执行失败,返回值为-1。
注意事项:
●ResumeThread函数用于恢复已被挂起的线程的执行,使线程从等待状态转为可执行状态。
●每次调用ResumeThread函数会减少线程的挂起计数。只有当挂起计数为0时,线程才会继续执行。
【注意】线程的挂起计数和线程同步,以避免死锁和竞争条件。
以下是一个简单的示例代码,演示如何使用ResumeThread函数恢复线程的执行:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
while (1) {
// 线程任务...
}
return 0;
}
int main() {
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, &dwThreadId);
if (hThread == NULL) {
printf("Failed to create thread.\n");
return 1;
}
// 挂起线程
DWORD suspendCount = SuspendThread(hThread);
if (suspendCount != -1) {
printf("Thread suspended. Suspend count: %lu\n", suspendCount);
} else {
printf("Failed to suspend thread.\n");
}
// 恢复线程
DWORD resumeCount = ResumeThread(hThread);
if (resumeCount != -1) {
printf("Thread resumed. Suspend count: %lu\n", resumeCount);
} else {
printf("Failed to resume thread.\n");
}
CloseHandle(hThread);
return 0;
}
在上述示例中,我们创建了一个线程,并在主函数中使用SuspendThread函数将线程挂起。然后,使用ResumeThread函数恢复线程的执行。通过分别检查挂起计数的返回值,可以确定函数是否执行成功,并获得线程的挂起计数。
需要注意的是,使用ResumeThread函数需要小心处理线程同步和死锁等问题。在实际应用中,更推荐使用更安全和可控的线程同步机制,如事件、互斥量、条件变量等。
19.2.6 第149练:多线程解决方案
/*------------------------------------------------------------------------
149 WIN32 API 每日一练
第149个例子MULTI2.C:使用多线程解决方案
GetDialogBaseUnits 函数
MoveWindow函数
DestroyWindow函数
(c) www.bcdaren.com 编程达人
-----------------------------------------------------------------------*/
#include <windows.h>
#include <math.h>
#include <process.h>//_beginthread函数要用到该头文件
typedef struct
{
HWND hwnd;
int cxClient;
int cyClient;
int cyChar;
BOOL bKill; //用来决定是否中止线程。
//主线程按ESC键时,在退出程序前,各线程根据这个
//标志退出线程函数,当然也就退出了线程。
}PARAMS, *PPARAMS;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //主窗口过程
//窗口过程1(显示自然数递增)
LRESULT APIENTRY WndProc1(HWND, UINT, WPARAM, LPARAM);
LRESULT APIENTRY WndProc2(HWND, UINT, WPARAM, LPARAM);//窗口过程2(质数递增)
LRESULT APIENTRY WndProc3(HWND, UINT, WPARAM, LPARAM);//窗口过程3(斐波那契数)
//窗口过程4(随机半径的圆)
LRESULT APIENTRY WndProc4(HWND, UINT, WPARAM, LPARAM);
void Thread1(PVOID pvoid); //线程函数1(显示自然数递增的)
void Thread2(PVOID pvoid); //线程函数2(质数递增)
void Thread3(PVOID pvoid); //线程函数3(斐波那契数)
void Thread4(PVOID pvoid); //线程函数3(随机半径的圆)
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Multi2");
…(略)
return msg.wParam;
}
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;
//创建4个子窗口
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_TIMER:
for (i = 0;i < 4;i++)
{
//每个子窗体发送WM_TIMER消息,也可以是其他自定义的消息,
//主要是触发绘图用的
SendMessage(hwndChild[i],WM_TIMER,wParam,lParam);
}
return 0;*/
case WM_CHAR:
if(wParam == '\x1B') //ESC
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
//KillTimer(hwnd,1);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd,message,wParam,lParam);
}
//检查是否到底行
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;
}
//窗口过程1:显示自然数递增
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());
params.bKill = FALSE;
_beginthread(Thread1,0,¶ms);//带额外参数,在线程函数中使用
return 0;
case WM_SIZE:
params.cxClient = LOWORD(lParam);
params.cyClient = HIWORD(lParam);
return 0;
case WM_DESTROY:
params.bKill = TRUE;
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd,message,wParam,lParam);
}
//线程函数1(显示自然数递增)
void Thread1(PVOID pvoid)
{
PPARAMS pParams;
TCHAR szBuffer[16];
HDC hdc;
int iLine = 0, iNum = 0;
pParams = (PPARAMS)pvoid;
while (!pParams->bKill) //用户如果没要出退出线程时
{
hdc = GetDC(pParams->hwnd);
//注意,这里该子窗口不清屏,会从上到下依次输出,
//直到最底部时,由CheckBottom判断,并清屏。
iLine = CheckBottom(pParams->hwnd, pParams->cyClient,
pParams->cyChar, iLine);
TextOut(hdc, 0, iLine*pParams->cyChar, szBuffer,
wsprintf(szBuffer, TEXT("%d"), iNum++));
iLine++;
ReleaseDC(pParams->hwnd, hdc);
}
_endthread(); //终止由 _beginthread 创建的线程.一般退出该线程函数后,
//线程会自动销毁,但在很复杂的线程
//环境中,用这个函数有时很有用
}
//窗口过程2:显示质数递增
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());
params.bKill = FALSE;
_beginthread(Thread2, 0, ¶ms);//带额外参数,在线程函数中使用
return 0;
case WM_SIZE:
params.cxClient = LOWORD(lParam);
params.cyClient = HIWORD(lParam);
return 0;
case WM_DESTROY:
params.bKill = TRUE;
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
void Thread2(PVOID pvoid)
{
int iNum = 1, iLine = 0, i ,iSqrt;
HDC hdc;
PPARAMS pparams;
TCHAR szBuffer[16];
pparams = (PPARAMS)pvoid;
while (!pparams->bKill)
{
do
{
if (++iNum < 0)
iNum = 0;
iSqrt = (int)sqrt(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();
}
//窗口过程3:显示斐波那契列数
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());
params.bKill = FALSE;
_beginthread(Thread3, 0, ¶ms);//带额外参数,在线程函数中使用
return 0;
case WM_SIZE:
params.cxClient = LOWORD(lParam);
params.cyClient = HIWORD(lParam);
return 0;
case WM_DESTROY:
params.bKill = TRUE;
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
void Thread3(PVOID pvoid)
{
int iNum = 0, iNext = 1,iLine = 0,iTemp;
HDC hdc;
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();
}
//窗口过程4:显示随机半径的圆
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());
params.bKill = FALSE;
_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;
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
void Thread4(PVOID pvoid)
{
int iDiameter;
HDC hdc;
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();
}
运行结果:
图19-4 多线程解决方案
总结
实例MULTI2.C的逻辑非常简单,在4个子窗口过程中分别创建4个独立的线程在子窗口完成绘制任务。
MULTI2.C不再设定Windows计时器或处理WM_TIMER消息。MULTI2中的大变化在于每个子窗口过程在处理WM_CREATE消息时都通过调用 _begimhread函数创立了一个单独的执行线程。MULTI2程序共有五个线程并行执行。主线 程包括主窗口过程和四个了窗口过程。另外四个线程使用函数Thread1、Thread2、Thread3、Thread4这些线程负责绘制四个子窗口。
在RNDRCTMT程序中的多线程代码没有使用_beginthread的第三个参数。这个参数可以让创立了另外一个线程的父线程给被创建线程传递一个32位的用来传递信息的变量。通 常,这个变最是一个指针,最常见的是一个指向某个结构的指针。这使线程的创立者和新线程不需要全局变量就可以共亨信息。你可以看到,MULTI2没有使用全局变量。
对MULTI2程序,我们在程序的开始定义了一个叫PARAMS的结构和一个指向该结构的指针PPARAMS。这个结构有五个字段:一个窗口句柄、窗口的宽度、窗口和高度、字符的高度和一个叫bKill的布尔变量。这个最后的字段用来让线程创立者通知新创立的线程何时终止。
让我们来看一看用来显示一串递增数字的子窗口过程WndProc1。这个窗口过程很简单.。唯一的局部变量是一个PARAMS结构。在处理WM_CREATE消息时,它设定该结构的hwnd和cyChar字段,调用_beginthread函数创立一个使用Threadl函数的新线程并且传递给新线程一个指向该PARAMS结构的指针。在处理WM_SIZE消息时,WndProc1设定该结构的cyClient字段。在处理WM_DESTROY消息时,它把bKill设置为TRUE。Threadl 通过调用_endthread来结束执行。这并不是必须的,因为在线程函数退出后,线程会被自动销毁。但是,如果线程处于一个很复杂的处理过程中,调用_endthread是很有用的。
Thread1函数执行窗口的实际绘制工作。它和程序中的其他四个线程并行执行。这个函 数接受一个指向PARAMS结构的指针,执行一个while循环,在每次循环时检査bKill字段是TRUE还是FALSE。如果是FALSE,函数执行和MULTI1.C中处理WM_TIMER消息的同样操作——格式化数字,获取设备环境句柄,并调用TextOut显示数字。
当你运行MULTI2时,你会看到窗口更新比MULTI1快得多。这说明程序更有效地利用了处理器。MULT11和MULTI2还有另外一个区别:当你移动窗口或改变窗口大小时,默认的窗口过程会进入一个模态循环,所有到窗口的输出会停止。而在 MULTI2中,输出会继续。