Win32 多线程程序设计(三)快跑与等待

3.1 busy waiting

使用 GetExitCodeThread() 可以决定一个线程是否还在执行。只要持续不断地检查 GetExitCodeThread() 的返回值,就可以等待某个线程结束。如果你没有等待线程结束就莽撞地结束程序,线程会被系统强制结束掉——在它完成它的工作之前。

如果只有一或两个线程以这种方式等待,倒也还好。但如果你有两百个线程都是靠不断地调用 GetExitCodeThread() 来等待结束呢?突然之间你会发现,CPU 的用途似乎都没有花在刀口上。

#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>
#include "MtVerify.h"

DWORD WINAPI ThreadFunc(LPVOID);

int main()
{
    HANDLE hThrd;
    DWORD exitCode = 0;
    DWORD threadId;
    DWORD begin;
    DWORD elapsed;

    puts("Timing normal function call...");
    begin = GetTickCount();
    ThreadFunc(0);
    elapsed = GetTickCount()-begin;
    printf("Function call took: %d.%.03d seconds\n\n", elapsed/1000, elapsed%1000);
    
    puts("Timing thread + busy loop...");
    begin = GetTickCount();

    MTVERIFY( hThrd = CreateThread(NULL,
        0,
        ThreadFunc,
        (LPVOID)1,
        0,
        &threadId )
    );
    /* This busy loop chews up lots of CPU time */
    for (;;)
    {
        GetExitCodeThread(hThrd, &exitCode);
        if ( exitCode != STILL_ACTIVE )
            break;
    }

    elapsed = GetTickCount()-begin;
    printf("Thread + busy loop took: %d.%.03d seconds\n", elapsed/1000, elapsed%1000);

    MTVERIFY( CloseHandle(hThrd) );

    return EXIT_SUCCESS;
}


DWORD WINAPI ThreadFunc(LPVOID n)
{
    int i;
    int inside = 0;
    double val;

    UNREFERENCED_PARAMETER(n);

    srand( (unsigned)time( NULL ) );

    for (i=0; i<1000000; i++)
    {
        double x = (double)(rand())/RAND_MAX;
        double y = (double)(rand())/RAND_MAX;
        if ( (x*x + y*y) <= 1.0 )
            inside++;
    }
    val = (double)inside / i;
    printf("PI = %.4g\n", val*4);
    return 0;
}

执行结果如下:

Timing normal function call...
PI = 3.139
Function call took: 0.063 seconds

Timing thread + busy loop...
PI = 3.139
Thread + busy loop took: 0.078 seconds
请按任意键继续. . .

操作系统没有能力分辨哪个线程的工作是有用的,哪个线程的工作是比较没有用的,所以每个线程获得一律平等的 CPU 时间。本例的主线程使用其所获得的 CPU 时间,疯狂地检查GetExitCodeThread() 的传回值,每秒数百万次。吓,一点生产力都没有!

3.2 等待一个线程结束

DWORD WaitForSingleObject(
    HANDLE hHandle,
    DWORD dwMilliseconds
);

参数:
hHandle:等待对象的 handle (代表一个核心对象)。在本例中,此为线程 handle 。
dwMilliseconds:等待的最长时间。时间终了,即使 handle 尚未成为激发状态,此函数还是要返回。此值可以是 0(代表立刻返回),也可以是INFINITE 代表无穷等待

返回值:
如果函数失败,则传回WAIT_FAILED。这时候你可调用 GetLastError() 取
得更多信息。此函数的成功有三个因素:
1. 等待的目标(核心对象)变成激发状态。这种情况下返回值将为WAIT_OBJECT_0。
2. 核心对象变成激发状态之前,等待时间终了。这种情况下返回值将为WAIT_TIMEOUT。
3. 如果一个拥有 mutex(互斥器)的线程结束前没有释放 mutex,则传回 WAIT_ABANDONED。

三个线程,没一个线程随机等待一段时间结束

#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include "MtVerify.h"

DWORD WINAPI ThreadFunc(LPVOID);

#define THREAD_POOL_SIZE 3

int main()
{
    HANDLE  hThrds[THREAD_POOL_SIZE];
    DWORD   threadId;
    int     i;
    DWORD   exitCode;

    for (i=0; i<THREAD_POOL_SIZE; i++)
    {
        MTVERIFY( hThrds[i] = CreateThread(NULL,
            0,
            ThreadFunc,
            (LPVOID)i,
            0,
            &threadId ) );
        printf("Launched thread #%d\n", i);
    }
    for (i=0; i<THREAD_POOL_SIZE; i++)
    {
        WaitForSingleObject(hThrds[i], INFINITE);
        MTVERIFY( GetExitCodeThread(hThrds[i], &exitCode) );
        printf("Slot %d terminated\n", exitCode );
        MTVERIFY( CloseHandle(hThrds[i]) );
    }
    printf("All slots terminated\n");

    return EXIT_SUCCESS;
}

DWORD WINAPI ThreadFunc(LPVOID n)
{
    srand( GetTickCount() );

    Sleep((rand()%8)*500+500);
    printf("Slot %d idle\n", n);
    return ((DWORD)n);
}

执行结果如下:

Launched thread #0
Launched thread #1
Launched thread #2
Slot 0 idle
Slot 0 terminated
Slot 1 idle
Slot 2 idle
Slot 1 terminated
Slot 2 terminated
All slots terminated
请按任意键继续. . .

有个严重的问题就是效率低,因为它假设线程的结束次序和被产生的次序相同,实际却不是。

3.3 等待多个线程

WaitForMultipleObjects() 允许你在同一时间等待一个以上的对象。可以解决上述问题。

DWORD WaitForMultipleObjects(
    DWORD nCount,
    CONST HANDLE *lpHandles,
    BOOL bWaitAll,
    DWORD dwMilliseconds
);

参数:
nCount  表示 lpH andles 所指之 handles 数组的元素个数。最大容量是 MAXIMUM_WAIT_OBJECTS 。
lpHandles  指向一个由对象 handles 所组成的数组。这些handles 不需要为相同的类型。
bWaitAll  如果此为 TRUE ,表示所有的 handles 都必须激发,此函数才得以返回。否则此函数将在任何一个handle 激发时就返回。
dwMilliseconds  当该时间长度终了时,即使没有任何 handles 激发,此函数也会返回。此值可为 0,以便测试。亦可指定为 INFINITE ,表示无穷等待。

返回值:
WaitForMultipleObjects() 的返回值有些复杂。
1. 如果因时间终了而返回,则返回值是 WAIT_TIMEOUT ,类似WaitForSingleObject()。
2. 如果 bWaitAll 是 TRUE ,那么返回值将是 WAIT_OBJECT_0 。
3. 如果 bWaitAll 是 FALSE,那么将返回值减去 WAIT_OBJECT_0,就表示数组中的哪一个 handle 被激发了。
4. 如果你等待的对象中有任何 mutexes,那么返回值可能从WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1。
5. 如果函数失败,它会传回WAIT_FAILED。这时候你可以使用GetLastError() 找出失败的原因。

以 WaitForMultipleObjects 取 代WaitForSingleObject(),并指定 bWaitAll 参数为 FALSE 。

#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>
#include "MtVerify.h"

DWORD WINAPI ThreadFunc(LPVOID);


int main()
{
    HANDLE hThreads[2];
    DWORD dwThreadId;
    DWORD dwExitCode = 0;
    int i;


    for (i=0; i<2; i++)
        MTVERIFY( hThreads[i] = CreateThread(NULL,
            0,
            ThreadFunc,
            (LPVOID)i,
            0,
            &dwThreadId )
        );

    Sleep(10000);

    WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);

    for (i=0; i<2; i++)
        MTVERIFY( CloseHandle(hThreads[i]) );
    printf("All terminated\n");

    return EXIT_SUCCESS;
}

DWORD WINAPI ThreadFunc(LPVOID n)
{
    srand( GetTickCount() );

    Sleep((rand()%8)*500+500);
    printf("Slot %d idle\n", n);
    return ((DWORD)n);
}

执行结果如下:

Slot 1 idle
Slot 0 idle
All terminated
请按任意键继续. . .

3.4 GUI中等待

“常常回到主消息循环”是十分重要的一件事。如果你没这么做,你的窗口就会停止重绘,你的程序菜单就不再有作用,用户不喜欢的事情则慢慢开始发生。问题是,如果你正使用 WaitForSingleOject()或 WaitForMultipleObjects()等待某个对象被激发,你根本没有办法回到主消息循环中去。

DWORD MsgWaitForMultipleObjects(
DWORD nCount,
LPHANDLE pHandles,
BOOL fWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask
);

参数:
dwWakeMask  欲观察的用户输入消息,可以是:QS_ALLINPUT、QS_HOTKEY、QS_INPUT、QS_KEY、QS_MOUSE、QS_MOUSEBUTTON、QS_MOUSEMOVE、QS_PAINT、QS_POSTMESSAGE、QS_SENDMESSAGE、QS_TIMER

返回值:
和 WaitForMultipleObjects()相比较,MsgWaitForMultipleObjects() 有一些额外的返回值意义。为了表示“消息到达队列”,返回值将是WAIT_OBJECT_0+nCount。

GetMessage会一直等待消息,知道有才返回,而PeekMessage则是查询不论有没有都会返回,而用MsgWaitForMultipleObjects可以先判断有没有消息,有消息再用PeekMessage。

while (GetMessage(&msg, NULL, 0, 0,))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}
while (!quit || gNumPrinting > 0)
{   // Wait for next message or object being signaled
    DWORD   dwWake;
    dwWake = MsgWaitForMultipleObjects(
                            gNumPrinting,
                            gPrintJobs,
                            FALSE,
                            INFINITE,
                            QS_ALLEVENTS);

    if (dwWake >= WAIT_OBJECT_0 && dwWake < WAIT_OBJECT_0 + gNumPrinting)
    {
        int index = dwWake - WAIT_OBJECT_0;
        gPrintJobs[index] = gPrintJobs[gNumPrinting-1];
        gPrintJobs[gNumPrinting-1] = 0;
        gNumPrinting--;
        SendMessage(hDlgMain, WM_THREADCOUNT, gNumPrinting, 0L);
    } 
    else if (dwWake == WAIT_OBJECT_0 + gNumPrinting)
    {
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {   
            if(hDlgMain == NULL || !IsDialogMessage(hDlgMain,&msg))
            {
                if (msg.message == WM_QUIT)
                {
                    quit = TRUE;
                    exitCode = msg.wParam;
                    break;
                } // end if
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        } // end while
    }
}

发表回复