Visual C++ 2017网络编程实战
上QQ阅读APP看书,第一时间看更新

3.2 利用Win32.API函数进行多线程开发

在用Win32 API线程函数进行开发之前,我们首先要熟悉这些API函数。常见的与线程有关的API函数见表3-2。

表3-2 与线程有关的API函数

3.2.1 线程的创建

在Win32 API中,创建线程的函数是CreateThread,该函数声明如下:

    HANDLE CreateThread(  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
     SIZE_T  dwStackSize,  LPTHREAD_START_ROUTINE  lpStartAddress,
     LPVOID  lpParameter,  DWORD  dwCreationFlags,  LPDWORD  lpThreadId);

其中,参数lpThreadAttributes是指向线程对象安全属性结构SECURITY_ATTRIBUTES的指针,该参数决定返回的句柄是否可以被子进程所继承,如果为NULL表示不能被继承;dwStackSize表示线程堆栈的初始大小,如果为零采用默认的堆栈大小;lpStartAddress指向线程函数的地址,线程函数就是线程创建后要执行的函数;lpParameter指向传给线程函数的参数;dwCreationFlags表示线程创建的方式,如果该参数为零,则线程创建后立即执行(就是立即执行线程函数),如果该参数为CREATE_SUSPENDED,则线程创建后不会执行,一直要等到调用函数ResumeThread后才会执行;lpThreadId指向一个DWORD变量,用来得到线程标识符(线程的ID)。如果函数成功,返回线程的句柄(严格地讲,应该是线程对象的句柄),若函数失败则返回NULL,可以用函数GetLastError来查看错误码。

CreateThread创建完子线程后,主线程会继续执行CreateThread后面的代码,这就可能会出现创建的子线程还没执行完主线程就结束了,比如控制台程序,主线程结束就意味着进程结束了。在这种情况下,我们就需要让主线程等待,等待子线程全部运行结束后再继续执行主线程。还有一种情况,主线程为了统计各个子线程的工作的结果而需要等待子线程结束完毕后再继续执行,此时主线程就要等待了。VC2017提供了等待函数来阻止某个线程的运行,直到某个指定的条件被满足,等待函数才会返回。如果条件没有满足,调用等待条件的函数将处于等待状态,并且不会占用CPU时间。

等待线程结束可以用等待函数WaitForSingleObject或WaitForMultipleObjects。前者用于等待一个线程对象的结束,后者用于等待多个线程对象的结束,但最多只能等待64个线程对象。这两个函数在线程同步中会详细解释。

线程创建之后,系统会为线程创建一个相关的内核对象—线程对象,该对象用线程句柄来引用,CreateThread如果创建成功后会返回线程对象句柄(简称线程句柄),并且该线程对象的引用计数加1。系统和用户可以利用线程句柄来对相应的线程进行必要的操纵,比如暂停、继续、等待完成等。如果我们不需要这些线程控制操作,则可以调用函数CloseHandle来关闭句柄,该函数声明如下:

    BOOL  CloseHandle(HANDLE  hObject);

其中,参数hObject是传入的线程句柄。如果函数成功就返回非零,否则返回零。

CloseHandle函数会使得线程对象的引用计数减1,当变为0时,系统删除该内核对象。关闭线程句柄和线程退出并没有联系,所以可以在线程退出之前关闭,甚至刚刚创建成功的时候关闭句柄,比如可以这样写:

    CloseHandle(CreateThread(…));

当然前提是不需要对线程进行控制的。如果不使用CloseHandle函数来关闭线程句柄,当整个应用程序结束时,系统也会对其进行回收,但这是一个不好的习惯。况且在很多情况下,我们在程序运行期间需要频繁地开启线程,如果不去关闭句柄就会导致系统资源越来越少,导致程序的不稳定。因此,每个线程句柄都应该要去关闭。

下面看一个例子,该例中会创建500个线程,每个线程函数中会向屏幕打印传入的线程参数。我们可以看到每个线程执行的时间不是固定的。

【例3.1】在控制台程序中创建线程

(1)新建一个控制台工程。

(2)在Test.cpp输入如下代码:

    #include "stdafx.h"
    #include <windows.h>
    #include <strsafe.h>

    #define MAX_THREADS 500 //要创建的线程个数
    #define BUF_SIZE 255

    typedef struct _MyData {  //定义传给线程的参数的类型
    int val1;
    int val2;
    } MYDATA, *PMYDATA;

    DWORD WINAPI ThreadProc(LPVOID lpParam) //线程函数
    {
    HANDLE hStdout;
    PMYDATA pData;

    TCHAR msgBuf[BUF_SIZE];
    size_t cchStringSize;
    DWORD dwChars;

    hStdout = GetStdHandle(STD_OUTPUT_HANDLE); //得到标准输出设备的句柄,为了打印
    if (hStdout == INVALID_HANDLE_VALUE)
         return 1;
    pData = (PMYDATA)lpParam; //把线程参数转为实际的数据类型
    //用线程安全函数来打印线程参数值
    StringCchPrintf(msgBuf, BUF_SIZE, _T("Parameters = %d, %d\n"), //构造字符串
         pData->val1, pData->val2);
    //得到字符串长度,存于cchStringSize
    StringCchLength(msgBuf, BUF_SIZE, &cchStringSize);
    //在终端窗口输出字符串
    WriteConsole(hStdout, msgBuf, cchStringSize, &dwChars, NULL);
    HeapFree(GetProcessHeap(), 0, pData); //释放分配的空间

    return 0;
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
    PMYDATA pData;
    DWORD dwThreadId[MAX_THREADS]; //线程ID数组
    HANDLE hThread[MAX_THREADS]; //线程句柄数组
    int i;
    printf("-----------begin----------------------\n");
    //创建MAX_THREADS个线程
    for (i = 0; i < MAX_THREADS; i++)
    {
         //为线程参数数据分配空间
         pData = (PMYDATA)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(MYDATA));
         if (pData == NULL) //如果分配失败,则结束进程
              ExitProcess(2);

         //为每个线程产生唯一的数据
         pData->val1 = i;
         pData->val2 = i;
         //创建线程
         hThread[i] = CreateThread(NULL,0, ThreadProc,pData,0,&dwThreadId[i]);
         if (hThread[i] == NULL) //如果创建失败则结束进程
              ExitProcess(i);
    }//for

    for (i = 0; i < MAX_THREADS; i++)
    {
         WaitForSingleObject(hThread[j], INFINITE); //等待第j个线程结束
    CloseHandle(hThread[i]); //线程创建后关闭对应的线程对象句柄,以释放资源
    }
    printf("-----------end----------------------\n");
    return 0;
    }

在上述代码中,我们首先用Win32 API函数HeapAlloc为线程参数的数据开辟空间,该函数在指定的堆上开辟一块内存空间。函数HeapAlloc分配的内存要用函数HeapFree来释放。CRT中的内存管理函数完全可以用Win32 API中的内存管理函数所代替。

在for循环里创建所有线程后,主线程会继续执行,由于我们在for后面调用了函数WaitForSingleObject来循环等待每一个线程的结束,因此主线程就一直在这里等待所有子线程运行结束,并且每当一个线程结束,就关闭其线程对象的句柄以释放资源。函数WaitForSingleObject用了参数INFINITE,表示无限等待的意思,只要子线程不结束,调用(该函数的)线程将一直等待下去。

在线程函数ThreadProc中,只是把传入的线程参数的结构体字段打印到控制台上。函数StringCchPrintf是sprintf的替代者,StringCchLength是strlen的替代者,CRT中的函数完全可以用Win32 API中的字符串处理函数所代替。

Win32 API函数GetStdHandle用来获取标准输出设备的句柄,最后由WriteConsole代替了CRT库中的printf函数,来打印输出到控制台窗口。这两个函数都是Win32中关于控制台编程的API函数。

再次强调,CreateThread创建的线程函数中最好不要使用CRT库函数,我们完全可以用对应的Win32 API函数来替代CRT库函数,上面的代码证实了这一点。

函数ExitProcess用来结束一个进程及其所有线程,声明如下:

    VOID ExitProcess(  UINT uExitCode);

其中,参数uExitCode是进程退出码,可以用API函数GetExitCodeProcess来获取它。

(3)保存工程并运行,运行结果如图3-1所示。

图3-1

由于我们打印了502行数据,而控制台窗口默认显示的行没有这么多,因此导致开始很多行数据没有显示,可以在图3-1的控制台窗口标题栏上右击,然后选择“属性”命令,通过属性对话框(见图3-2)来设置。在属性对话框中选择“布局”选项卡,然后在“屏幕缓冲区大小”下面的“高度”文本框中输入600,那么控制台窗口最多就可以显示600行了,最后别忘了单击“确定”按钮,然后重新运行我们的程序。

图3-2

3.2.2 线程的结束

线程的结束通常由以下原因所致:

(1)在线程函数中调用ExitThread函数。

(2)线程所属的进程结束了,比如进程调用了TerminateProcess或ExitProcess。

(3)线程函数执行结束后(return)返回了。

(4)在线程外部用函数TerminateThread来结束线程。

第1种方式最好不用,因为线程函数如果有C++对象,则C++对象不会被销毁;第2和4种方式尽量避免使用,因为它们不让线程有机会做清理工作、不会通知与线程有关的DLL、不会释放线程初始栈。第3种方式推荐使用,线程函数执行到return后结束,是最安全的方式,尽量应该将线程设计成这样的形式,即想让线程终止运行时,它们就能够return(返回)。当用该方式结束线程的时候,会导致下面事件的发生:

(1)在线程函数中创建的所有C++对象均将通过它们的撤销函数正确地撤销。

(2)操作系统将正确地释放线程堆栈使用的内存。

(3)与线程有关的DLL会得到通知,即DLL的入口函数(DllMain)会被调用。

(4)线程的结束状态从STILL_ACTIVE变为线程函数的返回值。

(5)由线程初始化的I/O等待都会取消。

(6)线程拥有的任何资源(比如窗口和钩子)都会得到释放。

(7)线程对象会被设为有信号状态,所以可以用函数WaitForSingleObject来等待线程的结束,比如:

    WaitForSingleObject(hThread, INFINITE);

(8)如果当前线程是进程中的唯一主线程,则线程结束的同时所属的进程也结束。

另外,线程结束时并不意味着线程对象会自动释放,必须调用CloseHandle来释放线程对象。

结束线程的函数有两个:一个是在线程内部使用的函数ExitThread,另外一个是在线程外部使用的函数TerminateThread。函数ExitThread声明如下:

    VOID ExitThread(DWORD dwExitCode);

其中,参数dwExitCode是传给线程的退出码,以后可以通过函数GetExitCodeThread来获取一个线程的退出码。该函数被调用的时候,线程堆栈会被释放。通常该函数在线程函数中调用。调用ExitThread函数来结束线程,通常会导致下列事件发生:

(1)如果线程函数中有C++对象,则C++对象得不到释放,因此有C++对象的线程函数不调用ExitThread。

(2)操作系统将正确地释放线程堆栈使用的内存。

(3)与线程有关的DLL会得到通知,即DLL的入口函数(DllMain)会被调用。

(4)线程的结束状态码为从STILL_ACTIVE变为dwExitCode参数确定的值。

(5)由线程初始化的I/O等待都会取消。

(6)线程拥有的任何资源(比如窗口和钩子)都会得到释放。

(7)线程对象会被设为有信号状态,所以可以用函数WaitForSingleObject来等待线程的结束,比如:

    WaitForSingleObject(hThread, INFINITE);

(8)如果当前线程是进程中的唯一主线程,则线程结束的同时所属的进程也会结束。

由此可见,只有第一条和线程函数返回结束时的情况不同。

函数GetExitCodeThread用来获取线程的结束状态值。该函数声明如下:

    BOOL GetExitCodeThread(HANDLE  hThread,  LPDWORD  lpExitCode);

其中,参数hThread是线程句柄;lpExitCode是一个指针,指向用于存放获取到的线程结束状态的变量。如果函数成功就返回非零,否则返回零。如果线程还没有结束,则获取到的结束状态值为STILL_ACTIVE,如果线程已经结束,则结束状态值可能是由函数ExitThread或TerminateThread的参数确定的值,或者是线程函数的返回值。

函数TerminateThread用来强制结束一个线程,这个函数尽量少用,因为它会导致一些线程资源没有机会释放。该函数声明如下:

    BOOL  TerminateThread( HANDLE  hThread,  DWORD  dwExitCode);

其中,参数hThread是要关闭的线程的句柄;dwExitCode为传给线程的退出码。如果函数成功就返回非零,否则返回零。该函数是一个危险的函数,非一些极端场合不要使用,比如线程中有网络阻塞函数recv,此时结束线程通常没有更好的办法,只能使用TerminateThread了。

下面看几个线程结束有关的例子。

【例3.2】得到线程的退出码

(1)新建一个控制台工程。

(2)在Test.cpp中输入如下代码:

       #include "stdafx.h"
       #include "windows.h"
       #include <strsafe.h>
       #define BUF_SIZE 255 //字符串缓冲区长度

       DWORD WINAPI ThreadProc(LPVOID lpParameter)
       {
        HANDLE hStdout;
        TCHAR msgBuf[BUF_SIZE]; //字符串缓冲区
        size_t cchStringSize; //存储字符串长度
        DWORD dwChars;

        hStdout = GetStdHandle(STD_OUTPUT_HANDLE);//得到标准输出设备的句柄,为了在终端打印
        if (hStdout == INVALID_HANDLE_VALUE)
             return 1;
        StringCchPrintf(msgBuf, BUF_SIZE, _T("线程ID = %d\n"),
GetCurrentThreadId());//构造字符串
       //得到字符串长度,存于cchStringSize
        StringCchLength(msgBuf, BUF_SIZE, &cchStringSize);
       //在终端窗口输出字符串
        WriteConsole(hStdout, msgBuf, cchStringSize, &dwChars, NULL);
       //在终端窗口输出字符串
        WriteConsole(hStdout, _T("线程即将结束\n"), 7, &dwChars, NULL);
        ExitThread(5); //结束本线程
        WriteConsole(hStdout, _T("这句话不会有机会打印了\n"), 12, &dwChars, NULL);
        return 0;
       }
       int _tmain(int argc, _TCHAR* argv[])
       {
        HANDLE h;
        DWORD dwCode,dwID;

        h = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwID); //创建子线程
        Sleep(1500); //主线程等待1.5秒
        GetExitCodeThread(h, &dwCode); //得到线程退出码
        printf("ID为%d的线程退出码:%d\n", dwID,dwCode); //输出结果
        CloseHandle(h); //关闭线程句柄
       }

函数GetCurrentThreadId可以在线程函数中得到本线程的ID,该值在CreateThread创建线程时确定,如果CreateThread函数最后一个参数为NULL,子线程也会有ID。

函数ExitThread设置了线程退出码为5,因此GetExitCodeThread函数得到的子线程的退出码为5。

(3)保存工程并运行,运行结果如图3-3所示。

图3-3

函数TerminateThread用来强制结束一个线程,声明如下:

    BOOL TerminateThread(  HANDLE  hThread,  DWORD  dwExitCode);

其中,参数hThread是要结束的线程的句柄;dwExitCode是传给线程的退出码,可以以后用函数GetExitCodeThread来获取该退出码。如果函数成功就返回非零,否则返回零。函数TerminateThread是具有危险性的函数,只应该在某些极端情况下使用。当TerminateThread结束线程时,线程将没有任何机会去执行用户模式下的代码以及释放初始栈。并且,依附在该线程上的DLL将不会被通知到该线程结束了。此外,如果要结束的目标线程拥有一个临界区,则临界区将不会被释放;如果要结束的目标线程从堆上分配了空间,则分配的堆空间将不会被释放。因此,这个函数尽量不去使用,比如下面的例子将产生死锁。

【例3.3TerminateThread结束线程导致死锁

(1)新建一个控制台工程。

(2)在Test.cpp中输入如下代码:

    #include "stdafx.h"
    #include "windows.h"
    DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
    char* p;
    while (1) //循环的分配和释放空间
    {
         p = new char[5];
         delete []p;
    }
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
    HANDLE h;
    char* q;

    h = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //创建子线程
    Sleep(1500); //主线程等待1.5秒
    TerminateThread(h, 0); //结束子线程
    q = new char[2]; //主线程中分配空间,但程序停在此行,不再执行下去,因为死锁了
    printf("分配成功\n");
    delete[]q;
    CloseHandle(h); //关闭线程句柄

    return 0;
    }

在上面的代码中,主线程执行到“q = new char[2];”时停滞不前了,因为发生了死锁。为什么会产生死锁呢?这是因为子线程中用了new/delete操作符向系统申请和释放堆空间,进程在其分配和回收内存空间时都会用到同一把锁。如果该线程在占用该锁时被杀死,即线程临死前还在进行new或delete操作,其他线程就无法再使用new或delete了,所以主线程中再用new时就无法成功执行了。

(3)保存工程并运行,运行结果如图3-4所示。

图3-4

这个例子说明一旦函数TerminateThread结束线程,线程函数就将立即结束,非常暴力。那么上面的例子应该如何让线程优雅地退出呢?简单的方法是用一个全局变量和WaitForSingleObject函数。

【例3.4】控制台下结束线程

(1)新建一个控制台工程。

(2)在Test.cpp中输入如下代码:

    #include "stdafx.h"
    #include "windows.h"

    BOOL gbExit=TRUE; //控制子线程中的循环是否结束

    DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
    char* p;
    while (gbExit)
    {
         p = new char[5];
         delete[]p;
    }
    return 0;
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
    HANDLE h;
    char* q;

    h = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //创建线程
    Sleep(1500); //主线程休眠一段时间,让出CPU给子线程运行一段时间

    gbExit = FALSE; //设置标记,让子线程中的循环结束,以结束子线程
    WaitForSingleObject(h, INFINITE); //等待子线程的退出

    h = NULL;
    q = new char[2]; //主线程中分配空间
    printf("分配成功\n");
    delete[]q; //释放空间
    CloseHandle(h); //关闭子线程句柄

    return 0;
    }

由于子线程结束的时候系统会向线程句柄发送信号,因此可以使用等待函数WaitForSingleObject来等待线程句柄的信号,一旦有信号了,就说明子线程结束,主线程就可以继续执行下去了。由于子线程是线程函数正常返回后退出的,因此new/delete的锁不再被占用,主线程就可以正常使用new/delete了。

(3)保存工程并运行,运行结果如图3-5所示。

图3-5

【例3.5】图形界面下结束线程

(1)新建一个对话框工程。

(2)切换到资源视图,打开对话框设计器,然后删除上面所有的控件,并添加两个按钮,标题分别设为“开启线程”和“结束线程”。接着为“开启线程”按钮添加事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton1()
    {
    // TODO:  在此添加控件通知处理程序代码
    CClientDC dc(this);
    dc.TextOut(0, 0, _T("线程已启动")); //在对话框上显示线程已启动
   GetDlgItem(IDC_BUTTON1)->EnableWindow(0); //设置“开启线程”按钮不可用
    gbExit = TRUE;
     ghThread = CreateThread(NULL, 0, ThreadProc, m_hWnd, 0, NULL); //创建线程
    }

ghThread是一个全局变量,保存线程句柄,定义如下:

    HANDLE ghThread;

然后添加线程函数:

    DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
    while (gbExit)
         ;
    return 0;
    }

代码很简单,gbExit是控制循环结束的全局变量,定义如下:

    BOOL gbExit = TRUE;

为“结束线程”添加事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton2()
    {
    // TODO:  在此添加控件通知处理程序代码
    if (!gbExit)
         return; //如果已经结束则直接返回
    gbExit = FALSE; //设置循环结束变量
    WaitForSingleObject(ghThread, INFINITE); //等待子线程退出
   CloseHandle (ghThread);//关闭线程句柄
   GetDlgItem(IDC_BUTTON1)->EnableWindow();//设置“开启线程”按钮可用
    CClientDC dc(this);
    dc.TextOut(0, 0, _T("线程已结束")); //在对话框上显示已经结束
    }

这种方式结束线程是优雅的,尽量不要使用TerminateThread函数来结束线程。

(3)保存工程并运行,运行结果如图3-6所示。

图3-6

3.2.3 线程和MFC控件交互

在MFC程序中,经常有这样的需求,需要把在线程计算的结果显示在某个MFC控件上,或者后台线程的工作时间较长,需要在界面上反映出它的进度。

那线程如何和界面打交道呢?或许有人会想到启动线程时把MFC控件对象的指针传参给线程函数,然后直接在线程函数中使用MFC控件对象并调用其方法来显示,但这种方式是不规范的,有可能会出现问题,因为MFC控件对象不是线程安全的,不能跨线程使用,或许此种方式在小程序中不会出问题,但是不出问题不等于没有问题,放在大型程序中早晚要出问题。再次强调:不要在子线程中操作主线程中创建的MFC控件对象,否则会带来意想不到的问题。在主线程中界面控件应该由主线程来控制,如果在子线程中也操作了界面控件,就会导致两个线程同时操作一个控件,若两个线程没有进行同步,就可能会发生错误。

那么在MFC程序中用Win32 API进行多线程开发时,应该如何和界面打交道呢?不同的情况有不同的处理方式。

如果仅仅是把线程计算的结果显示一下,第一种方法是把控件句柄传给线程,然后在线程函数中调用Win32 API函数或发送控件消息来操作控件;第二种方法是把界面主窗口的句柄传给线程,然后在线程函数中向主窗口发送自定义消息,接着可以在主窗口的自定义消息处理函数中调用控件对象的方法来操作控件。总之,如果涉及界面操作,应该传主窗口或控件窗口的句柄给子线程,而不要传主窗口或控件的对象指针,比如主窗口的this。另外,窗口句柄传给线程后,不要试图去通过句柄来获得窗口对象指针,比如想通过FromHandle函数把HWND转为(对话框)窗口对象指针:

       CMyDialog *pDlg = static_cast<CMyDialog*>(CWnd::FromHandle(reinterpret_cast<
HWND> ( pData ) ) );

这种情况系统会分配一个临时的窗口对象给你,而不是真正的主窗口对象。原因是强调HWND和CWnd的映射关系只能在一个线程模块(THREAD_MODULE_STATE)中使用,即不能跨线程同时也不能跨模块转换两者。

如果后台工作比较耗时,用户希望它尽快完成工作并且想知道其处理进度,则不能在线程函数中发送消息来更新界面,因为这样会拖慢子线程的工作速度。此时,应该设置一个进度变量(比如一个全局变量),放在子线程中不断累加,而在主线程中采用每隔一段时间去获取该变量值,并转换成百分比,然后把百分比以字符串或进度条的形式显示在界面上。这种方式相当于主线程主动轮询的方式,但界面操作依然是在主线程中完成。如果要增强同步程度,可以把间隔时间设置短一点,但代价也是降低工作效率。或许有人想完全和计算进度同步,想在线程函数中每计算一步就发送一个界面更新消息去反映一次进度,但这样会拖慢线程工作的计算效率,如果你的线程计算需要追求速度。这是因为SendMessage是一个阻塞函数,必须要等界面更新完毕后才能返回,在这个过程中线程就阻塞在那里了。有人或许又想到了非阻塞发送消息函数PostMessage,这个将直接导致界面死掉。比如:

    DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
    HWND hPos = (HWND)lpParameter; //获取进度条控件的句柄
    i = 0;
    for (i = 0; i < 88;i++) //循环做88次计算工作
    {
         myComputeWork();//计算工作
         ::PostMessage(hPos,PBM_SETPOS, i, 0); //发送设置进度的消息
         //Sleep(1);
    }

    return 0;
    }

在上面的线程函数中,不停地循环做计算工作myComputeWork,并且每计算完一次,就向进度条控件发送一次进度前进的消息。由于PostMessage是非阻塞函数,它向消息队列扔一条消息后就会立即返回,而界面操作通常比较慢,因此线程函数的循环向消息队列扔PBM_SETPOS消息会非常快,导致线程结束前消息队列中其他界面消息(比如鼠标点击、菜单操作等)无法进入消息队列,就不能接收用户操作了,看起来就像卡死了。如果我们让线程函数慢点扔消息呢?比如在PostMessage后面加一个Sleep函数,这样界面虽然不会卡死了,但是,通常这种循环计算工作用户对速度都是有要求的,人为的减慢将是不可接受的。虽然每隔一段时间去轮询进度的方法不能完全同步线程计算工作,但通常用户不会对此有严格的要求,只要有一个大概的反映进度就可以了。

下面我们看几个例子来加深一下理解。第一个例子通过在线程中把计算结果向控件发送控件消息来显示。第二个例子向主窗口发送自定义消息,然后在自定义消息处理函数中调用控件对象的方法来显示结果。两个例子传给线程函数的都是窗口句柄。相比较而言,第二种方法更简单些,因为控件消息大家使用起来不习惯,尤其对于SDK编程不熟悉的人来讲,更喜欢用MFC的方式来操作控件。

【例3.6】发送控件消息在状态栏中显示线程计算的结果

(1)新建一个单文档工程。

(2)切换到资源视图,打开菜单设计器,然后在“视图”菜单下添加菜单项“开始计算”,ID为ID_WORK。当用户点击该菜单项的时候,将开启一个线程,线程中将进行一个计算工作,然后把计算结果发送控件消息显示到状态栏上去。

为“开始计算”菜单项添加CMainFrame类下的事件处理函数:

    void CMainFrame::OnWork()
    {
    // TODO:  在此添加命令处理程序代码
    CreateThread(NULL, 0, ThreadProc, m_wndStatusBar.m_hWnd, 0, NULL); //创建线程
    }

代码很简单,使用API函数CreateThread来创建一个线程:线程函数是ThreadProc,线程参数是状态栏的句柄m_wndStatusBar.m_hWnd。由于LPVOID类型占用4个字节,而m_hWnd也占用4个字节,所以可以把句柄直接给LPVOID。

(3)在MainFrame.cpp中添加一个全局的线程函数,代码如下:

    DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
    HWND hwnd = (HWND)lpParameter; //把参数转为句柄
    int nCount = 4; //定义状态栏的4个部分
    //定义状态栏每个部分的大小,每个元素是每部分右边的纵坐标
    int array[] = { 100, 200, 300, -1 };
    //向状态栏发送分割部分消息,把状态栏分为4个部分,array存放每部分右边的纵坐标
    ::SendMessage(hwnd, SB_SETPARTS, (WPARAM)nCount, (LPARAM)array);
    //把计算结果发送给控件
    ::SendMessage(hwnd, SB_SETTEXT, (LPARAM)0, (WPARAM)TEXT("1+1=2"));
    ::SendMessage(hwnd, SB_SETTEXT, (LPARAM)1, (WPARAM)TEXT("2+2=4"));
    ::SendMessage(hwnd, SB_SETTEXT, (LPARAM)2, (WPARAM)TEXT("3+3=4"));
    ::SendMessage(hwnd, SB_SETTEXT, (LPARAM)3, (WPARAM)TEXT("4+4=8"));
    return 0;
    }

我们把状态栏分为4个部分,每个部分显示一个计算结果,当然这里也没有什么计算过程,直接把计算结果发送出去了。数组array存放每部分右边的纵坐标,这个坐标是客户区坐标,都是相对于客户区左边的,最后一个元素“-1”表示状态栏剩下部分的纵坐标一直持续到状态栏右边结束。消息SB_SETPARTS是状态栏进行分割的消息,SB_SETTEXT是为状态栏某个部分设置文本的消息。

(4)定位到函数CMainFrame::OnCreate,把该函数中的一个语句注释掉:

    //m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));

这条语句是框架用来切分状态栏部分的。为了防止和我们分割状态栏发生冲突,所以注释掉,否则每次最大化窗口的时候我们的分割就会失效。

(5)保存工程并运行,运行结果如图3-7所示。

图3-7

【例3.7】发送自定义消息在状态栏中显示线程计算的结果

(1)新建一个单文档工程。

(2)切换到资源视图,打开菜单设计器,然后在“视图”菜单下添加菜单项“开始计算”,ID为ID_WORK。当用户点击该菜单项的时候,将开启一个线程,并把主框架窗口的句柄传给线程函数,线程函数中将把计算结果向主框架窗口发送自定义消息,然后在自定义消息处理函数中调用状态栏对象的方法来显示结果。

为“开始计算”菜单项添加CMainFrame类下的事件处理函数:

    void CMainFrame::OnWork()
    {
    // TODO:  在此添加命令处理程序代码
    CreateThread(NULL, 0, ThreadProc, m_hWnd, 0, NULL); //创建线程
    }

代码很简单,就用API函数CreateThread来创建一个线程,线程函数是ThreadProc,线程参数是主框架窗口的句柄m_hWnd。由于LPVOID类型占用4个字节,而m_hWnd也占用4个字节,因此可以把句柄直接给LPVOID。接着添加线程函数:

    DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
    HWND hwnd = (HWND)lpParameter; //把参数转为句柄
    CString strRes = _T("结果是100b/s");
    ::SendMessage(hwnd, MYMSG_SHOWRES, WPARAM(&strRes), NULL);
    return 0;
    }

我们把CString对象的地址作为消息参数传给消息处理函数。MYMSG_SHOWRES是在MainFrame.cpp开头定义的自定义消息,定义如下:

    #define MYMSG_SHOWRES  WM_USER +10

然后在消息映射表中添加消息映射:

    ON_MESSAGE(MYMSG_SHOWRES, OnShowRes)

OnShowRes是自定义消息MYMSG_SHOWRES的处理函数,定义如下:

    LRESULT CMainFrame::OnShowRes(WPARAM wParam, LPARAM lParam)
    {
    CString* pstr = (CString*)wParam;

    m_wndStatusBar.SetPaneInfo(1, 10001, SBPS_NORMAL, 300);
    m_wndStatusBar.SetPaneText(1, *pstr);

    return 0;
    }

该函数把接收到的字符串显示在状态栏第一个窗格上:SetPaneInfo用来设置状态栏第一个窗格的宽度为300,函数SetPaneText用来把收到的字符串显示在第一个窗格上。注意:窗格次序从0开始,最左边的是第0个窗格。

最后,在MainFrame.h中对该函数进行声明:

    afx_msg LRESULT OnShowRes(WPARAM wParam, LPARAM lParam);

该声明写在DECLARE_MESSAGE_MAP()前面,并且因为是消息处理函数,所以开头要加上afx_msg。

(3)保存工程并运行,运行结果如图3-8所示。

图3-8

【例3.8】主动轮询并显示线程工作的进度

(1)新建一个对话框工程。

(2)切换到资源视图,打开对话框编辑器,删除上面的所有控件,然后添加一个按钮和进度条控件,按钮标题是“开启线程”,并为两个控件添加控件变量,分别为m_btn和m_pos。为按钮添加事件处理函数:

    void CTestDlg::OnBnClickedButton1()
    {
    // TODO:  在此添加控件通知处理程序代码
    gjd = 0; //初始化线程工作进度变量
    m_btn.EnableWindow(0); //开启线程时按钮变灰
    m_pos.SetRange(0, 100); //设置进度条范围
    m_pos.SetPos(0); //设置进度条起点位置
    SetTimer(1, 50, NULL); //开启计时器,每隔50毫秒轮询一次进度
    //开启线程并关闭句柄
    CloseHandle(CreateThread(NULL, 0, ThreadProc, m_hWnd, NULL, NULL));
    }

其中,gjd是整型全局变量,用来记录线程的计算工作进度。由于我们不需要对线程进行控制,因此线程句柄可以开始就关闭了。

添加线程函数:

    DWORD WINAPI  ThreadProc(LPVOID lpParameter)
    {
    int  i=0;
    float res=0.01;
    CString strRes;
    HWND hwnd = (HWND)lpParameter; //把参数转为句柄
    for (i = 0; i < 88;i++)
    {
         res += myComputeWork();//计算工作
         gjd++;
    }

    //发送计算结果自定义消息更新界面
    strRes.Format(_T("计算结果%.2lf"), res);
    ::SendMessage(hwnd, MYMSG_SHOWRES, WPARAM(&strRes), NULL);

    return 0;
    }

代码很简单,循环88次做我们的计算工作,然后把计算结果组织成字符串并通过自定义消息发送出去,以此显示在界面上。myComputeWork是一个自定义的全局函数,定义如下:

    float myComputeWork()
    {
    int i=0,j;
    double d = 1.0;
    while (i < 2000)
    {
         i++;
         for (j = -600; j < 600; j++)
                  d += sin(0.01);
    }
    return d;
    }

因为使用了正弦函数sin,所以文件开头不要忘了包含math.h。

接着,添加自定义消息MYMSG_SHOWRES的定义、消息映射以及消息处理函数:

    LRESULT CTestDlg::OnShowRes(WPARAM wParam, LPARAM lParam)
    {
    CString* pstr = (CString*)wParam;
    KillTimer(1); //停止计时器
    m_pos.SetPos(100); //设置进度条最右边
    m_btn.EnableWindow(1); //让“开启按钮”使能
    CClientDC dc(this);
    dc.TextOut(0, 0, *pstr); //在对话框左上角显示结果字符串

    return 0;
    }

(3)为对话框添加计时器消息处理函数:

    void CTestDlg::OnTimer(UINT_PTR nIDEvent)
    {
    // TODO:  在此添加消息处理程序代码和/或调用默认值
    float f = gjd / 87.0;
    int per = f * 100;
    m_pos.SetPos(per);

    CDialogEx::OnTimer(nIDEvent);
    }

这是主动轮询的核心所在,我们用一个计时器每隔一段时间来获取进度变量的值,并换算成百分百,然后显示在进度条上。这样看起来进度条就和线程计算工作在几乎同时前进了。

(4)保存工程并运行,运行结果如图3-9所示。

图3-9

3.2.4 线程的暂停和恢复

在上面的程序中,线程句柄似乎没啥用,这小节讲述线程的暂停和恢复继续运行,线程句柄就很重要了。暂停线程执行的API函数是SuspendThread,声明如下:

    DWORD SuspendThread(  HANDLE  hThread);

其中,参数hThread是要暂停的线程句柄,该句柄必须要有THREAD_SUSPEND_RESUME访问权限。如果函数成功就返回以前暂停的次数,否则返回-1,此时可以用GetLastError来获得错误码。当函数成功的时候,线程将暂停执行,并且线程的暂停次数递增一次。每个线程都有一个暂停计数器,最大值为MAXIMUM_SUSPEND_COUNT,如果暂停计数器大于零,线程则暂停执行。另外,这个函数一般不用于线程同步,如果对一个拥有同步对象(比如信号量或临界区)的线程调用SuspendThread函数,则有可能会引起死锁,尤其当被暂停的线程想要获取同步对象的时候。

恢复线程执行的函数是ResumeThread,但不是说调用该函数线程就会恢复执行,该函数主要是减少暂停计数器的次数。线程的暂停计数器如果恢复到零,线程才会恢复执行。该函数声明如下:

    DWORD ResumeThread(  HANDLE  hThread);

其中,参数hThread是要减少暂停次数的线程句柄,该句柄必须要有THREAD_SUSPEND_RESUME访问权限。如果函数成功就返回以前的暂停次数,若返回值大于1,则表示线程依旧处于暂停状态,如果函数失败就返回-1,此时可以用GetLastError来获得错误码。函数ResumeThread会检查线程的暂停计数器,如果ResumeThread返回值为零,就说明线程当前没有暂停;如果ResumeThread返回值大于1,则暂停计数器减1,且线程依旧处于暂停状态中;如果ResumeThread返回值为1,则暂停计数器减1,并且原来暂停的线程将恢复执行。

下面我们来看一个图形界面的例子,演示这几个函数的使用。

【例3.9】线程的暂停、恢复和中途终止

(1)新建一个对话框工程。

(2)切换到资源视图,打开对话框编辑器,删除上面的所有控件,然后添加4个按钮和进度条控件,4个按钮标题是“开启线程”“暂停线程”“恢复线程”和“结束线程”,并为“开启线程”按钮和进度条控件添加控件变量(分别为m_btn和m_pos)。为“开启线程”按钮添加事件处理函数:

    void CTestDlg::OnBnClickedButton1()
    {
    // TODO:  在此添加控件通知处理程序代码
    gjd = 0; //初始化线程工作进度变量
    gbExit = FALSE; //结束线程函数中循环的全局变量
    m_btn.EnableWindow(0); //开启线程时按钮变灰
    m_pos.SetRange(0, 100); //设置进度条范围
    m_pos.SetPos(0); //设置进度条起点位置
    SetTimer(1, 50, NULL); //开启计时器,每隔50毫秒轮询一次进度
    //开启线程并关闭句柄
    ghThread = CreateThread(NULL, 0, ThreadProc, m_hWnd, NULL, NULL);
    }

其中,gjd是整型全局变量,用来记录线程的计算工作进度。由于我们不需要对线程进行控制,因此线程句柄可以开始就关闭了。gbExit是一个BOOL型的全局变量,用来控制线程函数中循环的结束。ghThread是一个全局变量,用来存放线程句柄。

添加线程函数:

    DWORD WINAPI  ThreadProc(LPVOID lpParameter)
    {
    int  i=0;
    float res=0.01;
    CString strRes;
    HWND hwnd = (HWND)lpParameter; //把参数转为句柄
    for (i = 0; i < 88;i++)
    {
         if (gbExit) //控制循环退出
               break;
         res += myComputeWork();//计算工作
         gjd++; // myComputeWork每执行一次,该进度变量就累加一次
    }

    if (gbExit) strRes.Format(_T("线程被中途结束掉了"), res);
    else strRes.Format(_T("计算结果%.2lf"), res);
    ::SendMessage(hwnd, MYMSG_SHOWRES, WPARAM(&strRes), NULL); //发送自定义消息

    return 0;
    }

代码很简单,循环88次做我们的计算工作,然后把计算结果组织成字符串并通过自定义消息发送出去,以此显示在界面上。gbExit是用来控制循环退出条件的,当用户点击“结束进程”的时候会置该变量为TRUE。myComputeWork是一个自定义的全局函数,模拟一个计算工作,定义如下:

    float myComputeWork()
    {
    int i=0,j;
    double d = 1.0;
    while (i < 2000)
    {
         i++;
         for (j = -600; j < 600; j++)
                  d += sin(0.01);
    }
    return d;
    }

因为使用了正弦函数sin,所以文件开头不要忘了包含math.h。

接着,添加自定义消息MYMSG_SHOWRES的定义、消息映射以及消息处理函数:

    LRESULT CTestDlg::OnShowRes(WPARAM wParam, LPARAM lParam)
    {
    CString* pstr = (CString*)wParam;
    KillTimer(1); //停止计时器
    CloseHandle(ghThread); //关闭进程句柄
    ghThread = NULL;
    m_pos.SetPos(100); //设置进度条最右边
    m_btn.EnableWindow(1); //让“开启按钮”使能
    CClientDC dc(this);
    dc.TextOut(0, 0, *pstr); //在对话框左上角显示结果字符串

    return 0;
    }

(3)为对话框添加计时器消息处理函数:

    void CTestDlg::OnTimer(UINT_PTR nIDEvent)
    {
    // TODO:  在此添加消息处理程序代码和/或调用默认值
    float f = gjd / 87.0;
    int per = f * 100;
    m_pos.SetPos(per);

    CDialogEx::OnTimer(nIDEvent);
    }

这是主动轮询的核心所在,我们用一个计时器每隔一段时间来获取进度变量的值,并换算成百分百,然后显示在进度条上。这样看起来进度条就和线程计算工作在几乎同时前进了。

(4)添加“暂停线程”按钮的事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton2()
    {
    // TODO:  在此添加控件通知处理程序代码
    if (ghThread)
         SuspendThread(ghThread);
    }

再添加“恢复线程”按钮的事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton3()
    {
    // TODO:  在此添加控件通知处理程序代码
    if (ghThread)
         ResumeThread(ghThread);
    }

再添加“结束线程”按钮的事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton4()
    {
    // TODO:  在此添加控件通知处理程序代码
    gbExit = TRUE; //通过全局变量来停止线程函数中的循环以此来结束线程
    }

(5)保存工程并运行,运行结果如图3-10所示。

图3-10

3.2.5 消息线程和窗口线程

前面所创建的线程没有消息循环,也没有在线程中创建窗口,通常把这种线程称为工作线程。其实函数CreateThread创建线程还可以拥有消息队列,甚至创建窗口。拥有消息队列的线程称为消息线程。消息线程有两种类型:创建了窗口的消息线程和没有创建窗口的消息线程,前者通常称为窗口线程(或UI线程)。窗口线程中既然创建了窗口,那也必须要有窗口过程函数,由窗口过程函数对窗口消息进行处理,并且窗口和消息循环要在一个线程中,因此大家不要跨线程处理MFC控件对象,每个控件都是一个窗口,都有各自的消息循环,只是支持MFC把它封装掉罢了。

要让一个线程成为消息线程,方法是在线程函数中创建消息循环,并在循环中调用API函数的GetMessage或PeekMessage。一旦在线程中调用了这两个函数,系统就会为线程创建一个消息队列,这样这两个函数就可以获取消息了。大家一定要明确:消息队列是系统创建的,消息循环是线程创建的。

函数GetMessage声明如下:

       BOOL GetMessage(LPMSG  lpMsg, HWND  hWnd,UINT  wMsgFilterMin, UINT
    wMsgFilterMax);

其中,参数lpMsg指向MSG结构,该结构存放从线程消息队列中获取到的消息;hWnd为收到的窗口消息所对应窗口的句柄,这个窗口必须属于当前线程,如果该参数为NULL,则函数将收到所属当前线程的任一窗口的窗口消息以及当前线程消息队列中窗口句柄为NULL的消息,因此如果该参数为NULL,则不管线程消息是不是窗口消息都将被收到,如果该参数为-1,则只会收到窗口句柄为NULL的消息;wMsgFilterMin指定所收到的消息值的最小值;wMsgFilterMax指定所收到的消息值的最大值,如果wMsgFilterMin和wMsgFilterMax为零,那么GetMessage将收到所有可得到的消息。如果函数收到的消息不是WM_QUIT,,就返回非零,否则返回零。要注意的是,如果GetMessage从消息队列中取不到消息,就不会返回而阻塞在那里,一直等到取到消息才返回。因此,当线程消息队列中没有消息时GetMessage使得线程进入IDLE状态,被挂起;当有消息到达线程时GetMessage被唤醒,获取消息并返回。另外,该函数获取消息之后将删除消息队列中除WM_PAINT消息之外的其他消息,而WM_PAINT则只有在其处理之后才被删除。GetMessage函数只有在接收到WM_QUIT消息时才返回0,此时消息循环退出。

函数PeekMessage的主要功能是查看消息队列中是否有消息,当然也可以取出消息。即使消息队列中没有消息,该函数也会立即返回。相对而言,实际开发中GetMessage用的多一点。

在没有窗口的消息线程中,消息循环通常这样写:

    while (GetMessage(&msg, NULL, NULL, NULL))
    {
        switch (msg.message)
        {
        case MYMSG1: //自定义的消息
             break;
        case MYMSG2: //自定义的消息
             break;
        }
    }

对于有窗口的消息线程,消息循环通常这样写:

    while (GetMessage(&msg, NULL, NULL, NULL))
    {
         TranslateMessage(&msg);//如果要字符消息,这句也要
         DispatchMessage(&msg); //把消息派送到窗口过程中去
    }

函数TranslateMessage将虚拟键消息转换为字符消息。函数DispatchMessage必须要有,它把收到的窗口消息回传给操作系统,由操作系统调用窗口过程函数对消息进行处理。

向线程发送消息可以使用函数SendMessage、PostMessage或PostThreadMessage。SendMessage和PostMessage根据窗口句柄来发送消息,所以如果要向某个线程中的窗口发送消息,可以使用SendMessage或PostMessage。需要注意的是,SendMessage要一直等到消息处理完才返回,所以如果它发送的消息不是本线程创建的窗口的窗口消息,则本线程会被阻塞;PostMessage则不会,它会立即返回。另外,如果PostMessage的句柄参数为NULL,则相当于向本线程发送一个非窗口的消息。

函数PostThreadMessage根据线程ID来向某个线程发送消息,声明如下:

    BOOL PostThreadMessage(DWORD idThread, UINT  Msg, WPARAM  wParam,  LPARAM
lParam);

其中,参数idThread为线程ID,函数就是向该ID的线程投递消息;参数Msg表示要投递消息的消息号;wParam和lParam为消息参数,可以附带一些信息。如果函数成功就返回非零,否则返回零。需要注意的是,目标线程必须要有一个消息循环,否则PostThreadMessage将失败。此外,PostThreadMessage发送的消息不需要关联一个窗口,这样目标线程就不需要为了接收消息而创建一个窗口了。

通常,PostThreadMessage用于消息线程。SendMessage或PostMessage用于窗口线程。

下面我们看几个例子来加深一下对这几个函数使用的理解。

【例3.10】PostThreadMessage发送消息给无窗口的消息线程

(1)新建一个对话框工程。

(2)切换到资源视图,打开对话框编辑器,删除上面所有的控件,然后添加3个按钮,标题分别是“创建线程”“发送线程消息1”和“发送线程消息2”。为“创建线程”按钮添加事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton2()
    {
    // TODO:  在此添加控件通知处理程序代码
    CloseHandle(CreateThread(NULL, 0, ThreadProc, NULL, NULL, &m_dwThID));
    }

线程函数是ThreadProc。因为我们不需要控制线程,所以创建线程后,马上调用函数CloseHandle关闭其句柄。线程ID保存在m_dwThID中,该变量是类CTestDlg的成员变量:

    DWORD m_dwThID;

在TestDlg.cpp开头定义两个自定义消息:

    #define MYMSG1 WM_USER+1
    #define MYMSG2 WM_USER+2

为“发送线程消息1”按钮添加事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton1()
    {
    // TODO:  在此添加控件通知处理程序代码
    CString str = _T("祖国");
    //向ID为m_dwThID的线程发送消息
    PostThreadMessage(m_dwThID, MYMSG1, WPARAM(&str),0);
    Sleep(100); //等待100毫秒
    }

把字符串str作为消息参数发送给线程函数,然后主线程等待100毫秒,这样可以让子线程有机会把字符串显示一下,如果不等待,因为PostThreadMessage函数会立即返回,所以函数OnBnClickedButton1会很快结束,则局部变量str会很快销毁,子线程将收不到字符串。

同样,为“发送线程消息2”按钮添加事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton3()
    {
    // TODO:  在此添加控件通知处理程序代码
    CString str = _T("强大");
    //向ID为m_dwThID的线程发送消息
    PostThreadMessage(m_dwThID, MYMSG1, WPARAM(&str), 0);
    Sleep(100); //等待100毫秒
    }

把字符串str作为消息参数发送给线程函数,然后主线程等待100毫秒。

(3)保存工程并运行,运行结果如图3-11所示。

图3-11

3.2.6 线程同步

线程同步是多线程编程中重要的概念。它的基本意思就是同步各个线程对资源(比如全局变量、文件)的访问。如果不对资源访问进行线程同步,就会产生资源访问冲突的问题。比如,一个线程正在读取一个全局变量,而读取全局变量的这个语句在C++语言中只是一条语句,但在CPU指令处理这个过程的时候需要用多条指令来处理这个读取变量的过程。如果这一系列指令被另外一个线程打断了,也就是说CPU还没有执行完全部读取变量的所有指令就去执行另外一个线程了,而另外一个线程却要对这个全局变量进行修改,修改完后又返回原先的线程,继续执行读取变量的指令,此时变量的值已经改变了,这样第一个线程的执行结果就不是预料的结果了。

因此,多个线程对资源进行访问,一定要进行同步。VC2017提供了临界区对象、互斥对象和事件对象和信号量对象等4个同步对象来实现线程同步。

下面我们来看一个线程不同步的例子。模拟这样一个场景,甲乙两个窗口在售票,一共10张票,每张票的号码不同,每卖出一张票,就打印出卖出票的票号。我们可以把开辟的两个线程当作两个窗口在卖票,如果线程没有同步,就可能会出现两个“窗口”卖出的“票”是相同的,就发生了错误。

【例3.11】不用线程同步的卖票程序

(1)新建一个控制台工程。

(2)在Test.cpp中输入main函数代码:

    int _tmain(int argc, _TCHAR* argv[])
    {
    int i;
    HANDLE h[2];

    for (i = 0; i < 2;i++)
         h[i] = CreateThread(NULL, 0, threadfunc, (LPVOID)i, 0, 0);
    for (i = 0; i < 2; i++)
    {
         WaitForSingleObject(h[i], INFINITE);
         CloseHandle(h[i]);
    }
    printf("卖票结束\n");
    return 0;
    }

首先开启两个线程,线程函数是threadfunc,并把i作为参数传入(为了区分不同的窗口)。最后无限等待两个线程结束,一旦结束就关闭其线程句柄。

在main函数上面输入线程函数和全局变量,代码如下:

       #define  BUF_SIZE 100
       int gticketId = 10; //当前卖出票的票号
       DWORD WINAPI threadfunc(LPVOID param)
       {
        HANDLE hStdout;
        DWORD  i,dwChars;
        size_t szlen;
        TCHAR chWin, msgBuf[BUF_SIZE];

        if (param == 0) chWin = _T('甲'); //甲窗口
        else chWin = _T('乙'); //乙窗口
        
        
        while (1)
        {
             if (gticketId <= 0) //如果票号小于等于零,就跳出循环
                 break;
             hStdout = GetStdHandle(STD_OUTPUT_HANDLE); //为了打印,得到标准输出设备的句柄
             if (hStdout == INVALID_HANDLE_VALUE)
             {
                 return 1;
             }
             //构造字符串
             StringCchPrintf(msgBuf, BUF_SIZE, _T("%c窗口卖出的车票号 = %d\n"), chWin,
    gticketId);
             StringCchLength(msgBuf, BUF_SIZE, &szlen);  //得到字符串长度
             WriteConsole(hStdout, msgBuf, szlen, &dwChars, NULL);
             gticketId--;//每卖出一张车票,车票就减少一张
        }
       }

线程不停地卖票,每次卖出一张票就打印出车票号,同时减少一张。

最后添加所需头文件:

    #include "windows.h"
    #include <strsafe.h> //字符串处理函数需要

(3)保存工程并运行,从运行结果(见图3-12)可以看出不同的窗口居然卖出了同号的车票,这就说明没有线程同步的话程序出现问题了。

图3-12

3.2.6.1 临界区对象

临界区对象通过一个所有线程共享的对象来实现线程同步。线程要访问被临界区对象保护的资源,必须先要拥有该临界区对象。如果另一个线程要访问资源,则必须等待上一个访问资源的线程释放临界区对象。临界区对象只能用于一个进程内的不同线程之间的同步。

临界区的意思是一段关键代码,执行代码相当于进入临界区。要执行临界区代码,必须先独占临界区对象。比如可以把对某个共享资源进行访问这个操作看作一个临界区,要执行这段代码(访问共享资源),必须先拥有临界区对象。临界区对象好比一把钥匙,只有拥有了这把钥匙才能对共享资源进行访问。如果这把钥匙在其他线程手里,则当前线程只能等待,一直等到其他线程交出钥匙。VC2017提供了几个操作临界区对象的函数。

(1)InitializeCriticalSection函数

该函数用来初始化一个临界区对象。函数声明如下:

    void  InitializeCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

其中,参数lpCriticalSection为指向一个临界区对象的指针。CRITICAL_SECTION是一个结构体,定义了和线程访问相关的控制信息,具体内容我们不需要去管,它定义在WinBase.h中。

通常使用该函数之前会先定义一个CRITICAL_SECTION类型的全局变量,然后把地址传入该函数。

(2)EnterCriticalSection函数

该函数用于等待临界区对象的所有权,如果能获得临界区对象,那么该函数返回,否则函数进入阻塞,线程进入睡眠状态,一直到拥有临界区对象的线程释放临界区对象。该函数声明如下:

    void  EnterCriticalSection( LPCRITICAL_SECTION  lpCriticalSection);

其中,参数lpCriticalSection为指向一个临界区对象的指针。

(3)TryEnterCriticalSection函数

该函数也是用于等待临界区对象的所有权,和EnterCriticalSection不同的是,函数TryEnterCriticalSection不管有没有获取到临界区对象所有权,都将立即返回,相当于一个异步函数。函数声明如下:

BOOL  TryEnterCriticalSection( LPCRITICAL_SECTION  lpCriticalSection);

其中,参数lpCriticalSection为指向一个临界区对象的指针。如果成功获取临界区对象所有权,函数就返回非零,否则返回零。

(4)LeaveCriticalSection函数

该函数用于释放临界区对象的所有权。声明如下:

    void  LeaveCriticalSection(  LPCRITICAL_SECTION  lpCriticalSection);

其中,参数lpCriticalSection为指向一个临界区对象的指针。需要注意的是,线程获得临界区对象所有权,在使用完临界区后必须调用该函数释放临界区对象的所有权,让其他等待临界区的线程有机会进入临界区。该函数通常和EnterCriticalSection函数配对使用,它们中间的代码就是临界区代码。

(5)DeleteCriticalSection函数

该函数用来删除临界区对象,释放相关资源,使得临界区对象不再可用。函数声明如下:

    void  DeleteCriticalSection(  LPCRITICAL_SECTION  lpCriticalSection);

其中,参数lpCriticalSection为指向一个临界区对象的指针。

下面我们对前面线程不同步的卖票例子进行改造,加入临界区对象,使得线程同步。

【例3.12】使用临界区对象同步线程

(1)新建一个控制台工程。

(2)在Test.cpp中输入如下代码:

    #include "stdafx.h"
    #include "windows.h"
    #include <strsafe.h>

    #define  BUF_SIZE 100 //输出缓冲区大小
    int gticketId = 10; //记录卖出的车票号
    CRITICAL_SECTION gcs; //定义临界区对象

    DWORD WINAPI threadfunc(LPVOID param)
    {
    HANDLE hStdout;
    DWORD  i, dwChars;
    size_t szlen;
    TCHAR chWin,msgBuf[BUF_SIZE];

    if (param == 0) chWin = _T('甲'); //甲窗口
    else chWin = _T('乙'); //乙窗口
    while (1)
    {
             EnterCriticalSection(&gcs);
             if (gticketId <= 0)
             {
                  LeaveCriticalSection(&gcs); //注意要释放临界区对象所有权
                  break;
             }

             hStdout = GetStdHandle(STD_OUTPUT_HANDLE); //得到标准输出设备的句柄,为了打印
             if (hStdout == INVALID_HANDLE_VALUE)
             {
                  LeaveCriticalSection(&gcs); //注意要释放临界区对象所有权
                  return 1;
             }
             //构造字符串
             StringCchPrintf(msgBuf, BUF_SIZE, _T("%c窗口卖出的车票号 = %d\n"), chWin,
    gticketId);            StringCchLength(msgBuf, BUF_SIZE, &szlen); //得到字符串长度
             WriteConsole(hStdout, msgBuf, szlen, &dwChars, NULL); //在终端打印车票号
             gticketId--;//车票减少一张
             LeaveCriticalSection(&gcs); 释放临界区对象所有权
             Sleep(1); //让出CPU,让另外的线程有机会执行
        }
        }
        int _tmain(int argc, _TCHAR* argv[])
        {
        int i;
        HANDLE h[2];

        InitializeCriticalSection(&gcs); //初始化临界区对象

        for (i = 0; i < 2; i++)
             h[i] = CreateThread(NULL, 0, threadfunc, (LPVOID)i, 0, 0); //开辟两个线程
        for (i = 0; i < 2; i++)
        {
            WaitForSingleObject(h[i], INFINITE); //等待线程结束
            CloseHandle(h[i]);
        }
        DeleteCriticalSection(&gcs); //删除临界区对象
        printf("卖票结束\n");
        return 0;
        }

程序中使用了临界区对象来同步线程。gcs是临界区对象,通常定义成一个全局变量。在线程函数中,我们把用到全局变量gticketId的地方都包围进临界区内,这样一个线程在使用共享的全局变量gticketId时,其他线程就只能等待了。

(3)保存工程并运行,可以看到每次卖出的车票的号码都是不同的,运行结果如图3-13所示。

图3-13

3.2.6.2 互斥对象

互斥对象也称互斥量(Mutex),它的使用和临界区对象有点类似。互斥对象不仅能保护一个进程内的共享资源,还能保护系统中进程间的资源共享。互斥对象属于系统内核对象。

只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下共享资源都不会同时被多个线程所访问。互斥对象的使用通常需要结合等待函数,当没有线程拥有互斥对象时,系统会为互斥对象设置有信号状态(相当于向外发送信号),此时若有线程在等待该互斥对象(利用等待函数在等待),则该线程可以获得互斥对象,此时系统会将互斥对象设为无信号状态(不向外发送信号),如果又有线程在等待,则只能一直等待下去,直到拥有互斥对象的线程释放互斥对象,然后系统重新设置互斥对象为有信号状态。

下面先介绍一下等待函数。我们前面已经接触过WaitForSingleObject函数,这个就是等待函数,类似的还有WaitForMultipleObjects。所谓等待函数,就是用来等待某个对象产生信号的函数,比如一个线程对象在线程生命期内是处于无信号状态的,当线程终止时系统会设置线程对象为有信号状态,因此我们可以用等待函数来等待线程的结束。类似的,互斥对象没有被任何线程拥有的时候,系统会将它设置为有信号状态,一旦被某个线程拥有,就会设为无信号状态。线程可以调用等待函数来阻塞自己,直到信号产生后等待函数才会返回,线程才会继续执行。

函数WaitForSingleObject用来等待某个对象的信号,它知道对象有信号或等待超时才返回,函数声明如下:

    DWORD  WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

其中,参数hHandle是对象句柄;dwMilliseconds表示等待超时的时间,单位是毫秒,如果该参数是0,那么函数测试对象信号状态后立即返回,如果参数是宏INFINITE,表示函数不设超时时间一直等待对象有信号为止。如果函数成功,那么函数返回值如下:

·WAIT_ABANDONED,表示指定的对象是互斥对象,该互斥对象在拥有它的线程结束时没有被释放,互斥对象的所有权将被赋予调用本函数的线程,同时互斥对象被设为无信号状态。

·WAIT_OBJECT_0,表示指定的对象处于有信号状态了。

·WAIT_TIMEOUT,表示等待超时,同时对象仍处于无信号状态。

如果函数失败,则返回WAIT_FAILED ((DWORD)0xFFFFFFFF),相当于-1。

WaitForMultipleObjects可以用来等待多个对象,但数目不能超过64。该函数相当于在循环中调用WaitForSingleObject,一般用WaitForSingleObject即可。

下面介绍与互斥对象有关的API函数。

(1)CreateMutex函数

该函数创建或打开一个互斥对象。声明如下:

    HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes,
    BOOL bInitialOwner, LPCTSTR lpName );

其中,参数lpMutexAttributes为指向PSECURITY_ATTRIBUTES结构的指针,该结构表示互斥的安全属性,主要决定函数返回的互斥对象句柄能否被子进程继承,如果该参数为NULL,则函数返回的句柄不能被子进程继承;bInitialOwner决定调用该函数创建互斥对象的线程是否拥有该互斥对象的所有权,如果该参数为TRUE,表示创建该互斥对象的线程拥有该互斥对象的所有权;lpName是一个字符串指针,用来确定互斥对象的名称,该名称区分大小写,长度不能超过MAX_PATH,如果该参数为NULL,则不给互斥对象起名(为互斥对象起名字的目的是在不同进程之间进行线程同步)。如果函数成功就返回互斥对象句柄,否则函数返回NULL。

(2)ReleaseMutex函数

该函数用来释放互斥对象的所有权,这样其他等待互斥对象的线程就可以获得所有权。函数声明如下:

    BOOL  ReleaseMutex( HANDLE hMutex);

其中,参数hMutex是互斥对象的句柄。如果函数成功就返回非零,否则返回零。需要注意的是,函数ReleaseMutex是用来释放互斥对象所有权的,并不是销毁互斥对象。当进程结束的时候,系统会自动关闭互斥对象句柄,也可以使用CloseHandle函数来关闭互斥对象句柄,当最后一个句柄被关闭的时候系统销毁互斥对象。

下面我们通过互斥对象实现线程同步来改写例3.11。

【例3.13】使用互斥对象同步线程

(1)新建一个控制台工程。

(2)在Test.cpp中输入如下代码:

    #include "stdafx.h"
    #include "windows.h"
    #include <strsafe.h>

    #define  BUF_SIZE 100 //输出缓冲区大小
    int gticketId = 10;  //记录卖出的车票号
    HANDLE ghMutex; //互斥对象句柄

    DWORD WINAPI threadfunc(LPVOID param)
    {
    HANDLE hStdout;
    DWORD  i, dwChars;
    size_t szlen;
    TCHAR chWin, msgBuf[BUF_SIZE];

    if (param == 0) chWin = _T('甲'); //甲窗口
    else chWin = _T('乙'); //乙窗口
    while (1)
    {
             WaitForSingleObject(ghMutex, INFINITE); //等待互斥对象有信号
             if (gticketId <= 0) //如果车票全部卖出了,则退出循环
             {
                  ReleaseMutex(ghMutex); //释放互斥对象所有权
                  break;
             }

             hStdout = GetStdHandle(STD_OUTPUT_HANDLE); //得到标准输出设备的句柄,为了打印
             if (hStdout == INVALID_HANDLE_VALUE)
             {
                  ReleaseMutex(ghMutex);
                  return 1;
             }
             //构造字符串
             StringCchPrintf(msgBuf, BUF_SIZE, _T("%c窗口卖出的车票号 = %d\n"), chWin,
gticketId);
             StringCchLength(msgBuf, BUF_SIZE, &szlen);
             WriteConsole(hStdout, msgBuf, szlen, &dwChars, NULL); //控制台输出
             gticketId--;//车票减少一张
             ReleaseMutex(ghMutex); //释放互斥对象所有权
             //Sleep(1); //这句可以不用了
        }
    }
   int _tmain(int argc, _TCHAR* argv[])
    {
    int i;
    HANDLE h[2];

    printf("使用互斥对象同步线程\n");
    ghMutex = CreateMutex(NULL, FALSE, _T("myMutex")); //创建互斥对象

    for (i = 0; i < 2; i++)
             h[i] = CreateThread(NULL, 0, threadfunc, (LPVOID)i, 0, 0); //创建线程
    for (i = 0; i < 2; i++)
    {
        WaitForSingleObject(h[i], INFINITE); //等待线程结束
        CloseHandle(h[i]); //关闭线程对象句柄
    }
    CloseHandle(ghMutex); //关闭互斥对象句柄
    printf("卖票结束\n");
    return 0;
    }

程序通过互斥对象来实现线程同步。主线程中首先创建互斥对象,并把句柄存在全局变量ghMutex中,创建的时候第二个参数是FALSE,意味着主线程不拥有该互斥对象所有权。在线程函数中,在用到共享的全局变量gticketId之前调用等待函数WaitForSingleObject来等待互斥对象有信号,一旦等到,就可以进行关于gticketId的操作了。等操作完毕后再用函数ReleaseMutex来释放互斥对象所有权,使得互斥对象重新有信号,这样其他等待该互斥对象的线程可以得以执行。

与例3.12使用临界区对象来实现线程同步相比,该例的线程函数中不需要用Sleep(1)来使得当前线程让出CPU,因为其他线程已经在等待信号对象的信号了,一旦拥有互斥对象的线程释放所有权,其他线程马上可以等待结束,得以执行。

(3)保存工程并运行,由运行结果(见图3-14)可以看出每次卖出的车票的号码都是不同的。

图3-14

3.2.6.3 事件对象

事件对象也属于系统内核对象。它的使用方式和互斥对象有点类似,但功能更多一些。当等待的事件对象有信号状态时,等待事件对象的线程得以恢复,继续执行;如果等待的事件对象处于无信号状态,则等待该对象的线程将挂起。

事件可以分为两种:手动事件和自动事件。手动事件的意思是当事件对象处于有信号状态时,它会一直处于这个状态,一直到调用函数将其设置为无信号状态为止。自动事件是指当事件对象处于有信号状态时,如果有一个线程等待到该事件对象的信号后,事件对象就变为无信号状态了。

事件对象也要使用等待函数,比如WaitForSingleObject。关于等待函数上一节已经介绍过了,这里不再赘述。

下面介绍有关事件对象的几个API函数。

(1)CreateEvent函数

该函数用于创建或打开一个事件对象,声明如下:

    HANDLE  CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL
bManualReset,BOOL bInitialState, LPCTSTR lpName);

其中,参数lpEventAttributes是指向SECURITY_ATTRIBUTES结构的指针,该结构表示一个安全属性,如果该参数为NULL,表示函数返回的句柄不能被子进程继承;bManualReset用于确定是创建一个手动事件还是一个自动事件;bInitialState用于指定事件对象的初始状态,如果为TRUE就表示事件对象创建后处于有信号状态,否则为无信号状态;lpName指向一个字符串,该字符串表示事件对象的名称,该名称字符串是区分大小写的,长度不能超过MAX_PATH,如果该参数为NULL,则表示创建一个无名字的事件对象,事件对象的名称不能和其他同步对象的名称(比如互斥对象的名称)相同。如果函数成功就返回新创建的事件对象句柄,否则返回NULL。

(2)SetEvent函数

该函数将事件对象设为有信号状态。

    BOOL  SetEvent(  HANDLE  hEvent);

其中,参数hEvent表示事件对象句柄。如果函数成功就返回非零,否则返回零。

(3)ResetEvent函数

该函数将事件对象重置为无信号状态。声明如下:

    BOOL  ResetEvent(HANDLE  hEvent);

其中,参数hEvent是事件对象句柄,如果函数成功就返回非零,否则返回零。

当进程结束的时候,系统会自动关闭事件对象句柄,也可以调用CloseHandle来关闭事件对象句柄,当与之关联的最后一个句柄被关掉后,事件对象被销毁。

下面我们通过事件对象实现线程同步来改写例3.11。

【例3.14】使用事件对象同步线程

(1)新建一个控制台工程。

(2)在Test.cpp中输入如下代码:

    #include "stdafx.h"
    #include "windows.h"
    #include <strsafe.h>

    #define  BUF_SIZE 100 //输出缓冲区大小
    int gticketId = 10;  //记录卖出的车票号
    HANDLE ghEvent; //事件对象句柄

    DWORD WINAPI threadfunc(LPVOID param)
    {
    HANDLE hStdout;
    DWORD  i, dwChars;
    size_t szlen;
    TCHAR chWin, msgBuf[BUF_SIZE];
    if (param == 0) chWin = _T('甲'); //甲窗口
    else chWin = _T('乙'); //乙窗口
    while (1)
    {
             WaitForSingleObject(ghEvent, INFINITE); //等待事件对象有信号
             if (gticketId <= 0) //如果车票全部卖出了,就退出循环
             {
                  SetEvent(ghEvent); //设置事件对象有信号
                  break;
             }
             hStdout = GetStdHandle(STD_OUTPUT_HANDLE); //得到标准输出设备的句柄,为了打印
             if (hStdout == INVALID_HANDLE_VALUE)
             {
                  SetEvent(ghEvent); //释放事件对象所有权
                  return 1;
             }
             //构造字符串
             StringCchPrintf(msgBuf, BUF_SIZE, _T("%c窗口卖出的车票号 = %d\n"), chWin,
gticketId);
             StringCchLength(msgBuf, BUF_SIZE, &szlen);
             WriteConsole(hStdout, msgBuf, szlen, &dwChars, NULL); //控制台输出
             gticketId--;//车票减少一张
             SetEvent(ghEvent); //设置事件对象有信号
             //Sleep(1); //这句可以不用了
        }
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
    int i;
    HANDLE h[2];
    printf("使用事件对象同步线程\n");
    ghEvent = CreateEvent(NULL, FALSE, TRUE,_T("myEvent")); //创建事件对象

    for (i = 0; i < 2; i++)
             h[i] = CreateThread(NULL, 0, threadfunc, (LPVOID)i, 0, 0); //创建线程
    for (i = 0; i < 2; i++)
    {
             WaitForSingleObject(h[i], INFINITE); //等待线程结束
             CloseHandle(h[i]); //关闭线程对象句柄
    }
    CloseHandle(ghEvent); //关闭事件对象句柄
    printf("卖票结束\n");
    return 0;
    }

程序利用事件对象来同步两个线程。首先创建一个事件对象,并在开始时设置有信号状态。然后在使用共享的全局变量gticketId之前需要等待,等到事件对象的信号后线程开始操作与gticketId有关的代码,同时事件对象处于无信号状态,一旦与gticketId有关操作完成就利用SetEvent函数设置事件对象为有信号状态,以便其他在等待事件对象的线程能得以执行。

(3)保存工程并运行,由运行结果(见图3-15)可以看出每次卖出的车票的号码都是不同的。

图3-15

3.2.6.4 信号量对象

信号量对象也是一个内核对象。它的工作原理是:信号量内部有计数器,当计数器大于零时,信号量对象处于有信号状态,此时等待信号量对象的线程得以继续进行,同时信号量对象的计数器减一;当计数器为零时,信号量对象处于无信号状态,此时等待信号量对象的线程将被阻塞。下面介绍和信号量操作有关的API函数。

(1)CreateSemaphore函数

该函数创建或打开一个信号量对象,声明如下:

    HANDLE  CreateSemaphore (LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
       LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);

其中,参数lpSemaphoreAttributes指向SECURITY_ATTRIBUTES结构的指针,该结构表示安全属性,如果为NULL,就表示函数返回的句柄不能被子进程继承;lInitialCount表示信号量的初始计数,该参数必须大于等于零,并且小于等于lMaximumCount;lMaximumCount指定信号量对象计数器的最大值,该参数必须大于零;lpName指向一个字符串,该字符串指定信号量对象的名称,区分大小写,并且长度不能超过MAX_PATH,如果为NULL,则创建一个无名信号量对象。如果函数成功就返回信号量对象句柄,如果指定名字的信号量对象已经存在,就返回那个已经存在的信号量对象的句柄,如果函数失败就返回NULL。

(2)ReleaseSemaphore函数

该函数用来为信号量对象的计数器增加一定数量,声明如下:

       BOOL  ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG
lpPreviousCount);

其中,参数hSemaphore为信号量对象句柄;lReleaseCount指定要将信号量对象的当前计数器增加的数目,该参数必须大于零,如果该参数使得计数器的值大于其最大值(在创建信号量对象的时候设定),计数器值将保持不变,并且函数返回FALSE;lpPreviousCount指向一个变量,该变量存储信号量对象计数器的前一个值。如果函数成功就返回非零,否则返回零。

下面我们通过信号量对象实现线程同步来改写例3.11。

【例3.15】使用信号量对象同步线程

(1)新建一个控制台工程。

(2)在Test.cpp中输入如下代码:

        #include "stdafx.h"
        #include "windows.h"
        #include <strsafe.h>

        #define  BUF_SIZE 100 //输出缓冲区大小
        int gticketId = 10;  //记录卖出的车票号
        HANDLE ghSemaphore; //信号量对象句柄

        DWORD WINAPI threadfunc(LPVOID param)
        {
        HANDLE hStdout;
        DWORD  i, dwChars;
        size_t szlen;
        LONG cn;
        TCHAR chWin, msgBuf[BUF_SIZE];

        if (param == 0) chWin = _T('甲'); //甲窗口
        else chWin = _T('乙'); //乙窗口
        while (1)
        {
             WaitForSingleObject(ghSemaphore, INFINITE); //等待信号量对象有信号
             if (gticketId <= 0) //如果车票全部卖出了,就退出循环
             {
                  ReleaseSemaphore(ghSemaphore,1,&cn); //释放信号量对象所有权
                  break;
             }

             hStdout = GetStdHandle(STD_OUTPUT_HANDLE); //得到标准输出设备的句柄,为了打印
             if (hStdout == INVALID_HANDLE_VALUE)
             {
                  ReleaseSemaphore(ghSemaphore,1, &cn); //释放信号量对象所有权
                  return 1;
             }
             //构造字符串
             StringCchPrintf(msgBuf, BUF_SIZE, _T("%c窗口卖出的车票号 = %d\n"), chWin,
gticketId);
             StringCchLength(msgBuf, BUF_SIZE, &szlen);
             WriteConsole(hStdout, msgBuf, szlen, &dwChars, NULL); //控制台输出
             gticketId--;//车票减少一张
             ReleaseSemaphore(ghSemaphore,1, &cn); //释放信号量对象所有权
             //Sleep(1); //这句可以不用了
        }
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
    int i;
    HANDLE h[2];
    printf("使用信号量对象同步线程\n");
    ghSemaphore = CreateSemaphore(NULL, 1, 50, _T("mySemaphore"));//创建信号量对象

    for (i = 0; i < 2; i++)
         h[i] = CreateThread(NULL, 0, threadfunc, (LPVOID)i, 0, 0); //创建线程
    for (i = 0; i < 2; i++)
    {
         WaitForSingleObject(h[i], INFINITE); //等待线程结束
         CloseHandle(h[i]); //关闭线程对象句柄
    }
    CloseHandle(ghSemaphore); //关闭信号量对象句柄
    printf("卖票结束\n");
    return 0;
    }

上面的代码通过信号量对象来同步两个线程。首先创建一个计数器为1的信号量对象,因为信号量计数器大于0,所以信号量对象处于有信号状态,然后在子线程中的等待函数就可以等到该信号,并且信号量对象计数器减一变为零,则其他等待函数就只能阻塞了,等到共享的全局变量gticketId操作完成后,让信号量对象计数器加1,计数器大于零了则信号量对象重新变为有信号状态,其他线程得以等待返回继续执行。

(3)保存工程并运行,运行结果如图3-16所示。

图3-16