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

3.3 CRT库中的多线程函数

CRT库的全称是C Run-time Libraries,即C运行时库,包含了C常用的函数(如printf、malloc、strcpy等),为运行main做了初始化环境变量、堆、IO等资源,并在结束后清理。在Windows环境下,VC2017提供的 C Run-time Libraries分为动态运行时库、静态运行时库、调试版本(Debug)、发行版本(Release)等,它们都是支持多线程的,以前老的VC版本还有单线程版本CRT,现在单线程版本CRT已经淘汰了。我们可以在IDE工程属性中进行设置,选择不同版本的CRT,比如打开工程属性对话框,然后在左边选择“C/C++”→“代码生成”,在右边的“运行库”旁边可以选择不同的CRT库,如图3-17所示。

图3-17

其中,/MT表示多线程静态链接的Release版本的CRT库,在LIBCMT.LIB中实现。/MTd表示多线程静态链接的Debug版本的CRT库,在LIBCMTD.LIB中实现。/MD表示多线程DLL的Release版本的CRT库,在MSVCRT.LIB中实现。/MDd表示多线程DLL的Debug版本的CRT库,在MSCVRTD.LIB中实现。通常这里保持默认即可。

CRT库中提供了创建线程和结束线程的函数,比如创建线程函数_beginthread和_beginthreadex、结束线程函数_endthread和_endthreadex。_beginthread和_endthread对应使用,_beginthreadex和_endthreadex对应使用。前面Win32 API函数CreateThread创建的线程中不应使用CRT库中的函数,现在_beginthread和_beginthreadex创建的线程则可以使用CRT库函数。其实,在_beginthread和_beginthreadex内部都调用了API函数CreateThread,但在调用该API函数前做了很多初始化工作,在调用后又做了不少检查工作,这使得线程能更好地支持CRT库函数。函数_endthread和_endthreadex的内部其实调用了API函数ExitThread,但它们还做了许多善后工作。

如果要在控制台程序下使用CRT中的线程函数,就要包括头文件process.h。

函数_beginthread声明如下:

    uintptr_t  _beginthread( void( *start_address )( void * ), unsigned  stack_size,
void *arglist );

其中,参数start_address是线程函数的起始地址,该线程函数的调用约定必须是__cdecl或__clrcall(用于托管);stack_size是线程的堆栈大小,如果为零,就使用系统默认值;arglist指向传给线程函数参数的指针。函数如果成功就返回线程句柄(根据平台不同,uintptr_t可能为unsigned integer或unsigned __int64),如果失败就返回-1。需要注意的是,如果创建的线程很快退出了,则_beginthread可能返回一个无效句柄。

_beginthread创建的线程可以用函数_endthread来结束,该函数声明如下:

    void _endthread();

如果在线程函数中使用_endthread,该函数后面的代码将得不到执行。此外,当线程函数返回的时候系统也会自动调用_endthread,并且_endthread会自动关闭线程句柄。正因为这个原因,我们不需要再去显式调用CloseHandle函数来关闭线程句柄,而且也不应该在主线程中使用等待函数(比如WaitForSingleObject)来等待子线程句柄的方式去判断子线程是否结束,比如如下代码可能会出现句柄无效的异常报错:

    WaitForSingleObject((HANDLE)ghThread1, INFINITE); //等待子线程退出
    CloseHandle((HANDLE)ghThread1);//关闭线程句柄

单步调式时很容易报错,如图3-18所示。

图3-18

正确的方式是如果要等待_beginthread创建的线程结束,就可以使用同步对象,比如事件等,后面的例子我们会演示。

函数_beginthreadex比_beginthread功能强大一些,并且更安全些,声明如下:

        uintptr_t _beginthreadex(void *security,  unsigned stack_size, unsigned
    ( *start_address )( void * ),void *arglist, unsigned initflag, unsigned *thrdaddr );

其中,参数security表示线程的安全描述符;stack_size是线程的堆栈大小,如果为零,就使用系统默认值;start_address是线程函数的起始地址,该线程函数的调用约定必须是__stdcall或__clrcall(用于托管);arglist指向传给线程函数参数的指针;initflag用于指示线程创建后是否立即执行,0表示立即执行,CREATE_SUSPENDED表示创建后挂起;thrdaddr指向一个32位的变量,该变量用来存放线程ID。函数如果成功就返回线程句柄(根据平台不同,uintptr_t可能为unsigned integer或unsigned __int64),如果失败就返回0。

_beginthread相当于_beginthreadex的功能子集,但是使用_beginthread既无法创建带有安全属性的新线程,也无法创建初始能暂停的线程,还无法获得线程ID。

_beginthreadex的功能类似于API函数CreateThread,虽然功能类似,但是推荐使用_beginthreadex,这是因为不少人对CRT函数更熟悉些,所以在线程函数中的某些需求经常会想用CRT函数去解决。前面提到过,在CreateThread创建的线程中使用CRT函数会产生一些内存泄漏。

_beginthreadex创建的线程可以使用函数_endthreadex来结束,如果在线程函数中调用_endthreadex,那么该函数后面的代码将都不会执行。同样,_beginthreadex创建的线程函数返回时,系统会自动调用_endthreadex,但_endthreadex并不会去关闭线程句柄,所以要开发者显式地调用CloseHanlde来关闭线程句柄。因为_endthreadex并不会去关闭线程句柄,所以可以在主线程中使用等待函数(比如WaitForSingleObject)来等待子线程句柄,以此判断子线程是否结束。_beginthreadex函数的使用流程和CreateThread几乎一样。

下面看几个小例子,第一个例子利用_beginthread函数不断创建线程,看最多能创建多少个线程。第二个例子和前面章节类似的卖票程序,用互斥对象来同步_beginthread函数创建的两个线程,这是一个控制台程序,在这个程序中我们要向控制台打印信息,可以直接使用CRT库中的printf函数,因为线程也是CRT库函数_beginthread创建的。

【例3.16】利用_beginthread不断创建线程

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

(2)切换到资源视图,打开对话框编辑器,删除上面所有的控件,然后添加4个按钮和2个静态控件,按钮的标题分别设为“启动”“暂停”“继续”和“结束线程”,一个静态控件的标题设为“已经创建的线程数:”,并把该静态控件放在左上角,然后把另外一个静态控件放在它的右边,并设ID为IDC_THREAD_COUNT。双击“启动”按钮,添加事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton1()
    {
    // TODO:  在此添加控件通知处理程序代码
    if (_beginthread(threadFunc1, 0, m_hWnd) != -1) //创建线程
         GetDlgItem(IDC_BUTTON1)->EnableWindow(FALSE); //按钮变为不可用
    if (!ghEvent)
         ghEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    }

一旦成功创建线程,按钮就变为不可用。其中,ghEvent是一个事件句柄,是全局变量,定义如下:

    HANDLE ghEvent = NULL;

通过这个事件句柄我们将用于等待子线程的退出。threadFunc1是线程函数,并把对话框句柄m_hWnd作为参数传给线程函数。threadFunc1函数的代码如下:

    void threadFunc1(void *pArg)
    {
    HWND hWnd = (HWND)pArg;
    g_nCount = 0;
    g_bRun = true;
    while (g_bRun) //不断地创建新的线程
    {
         if (_beginthread(threadFunc2, 0, hWnd) == -1)
         {
             g_bRun = false; //如果创建失败了,就置false,准备退出循环
             break;
         }
    }
    ::PostMessage(hWnd, WM_SHOW_THREADCOUNT, 1, 0); //发送消息通知,线程结束
    SetEvent(ghEvent); //设置事件状态
    }

代码很简单,就是不停地在循环中创建线程,一直到失败。需要注意的是,程序结尾用PostMessage ,不要用SendMessage,因为我们后面主线程会等待子线程的结束,等待的时候主线程会挂起,所以如果用SendMessage,SendMessage就会无法返回(因为主线程挂起了),这样子线程和主线程互相等待了。其中,g_nCount和g_bRun都是全局变量,定义如下:

    bool g_bRun = false; // 控制循环结束
    long g_nCount = 0; //统计所创建的线程个数

WM_SHOW_THREADCOUNT是自定义消息,定义如下:

    #define WM_SHOW_THREADCOUNT WM_USER+5

threadFunc2也是线程函数,定义如下:

    void threadFunc2(void *pArg)
    {
    HWND hWnd = (HWND)pArg;

    g_nCount++; //线程个数累加
    ::SendMessage(hWnd, WM_SHOW_THREADCOUNT, 0, g_nCount);//发送消息显示线程个数
    while (g_bRun) //如果程序还在创建线程,则每个子线程一直运行
         Sleep(1000);
    }

threadFunc2线程函数只是把当前已经创建的线程个数通过发送消息去显示。接着添加WM_SHOW_THREADCOUNT的消息处理函数:

    LRESULT CTestDlg::OnMyMsg(WPARAM wParam, LPARAM lParam)
    {
    CString str;
    if (wParam == 1)
         GetDlgItem(IDC_BUTTON1)->EnableWindow(TRUE);  //线程准备结束了,则让按钮使能
    else
    {
         str.Format(_T("%d"), g_nCount);
         GetDlgItem(IDC_THREAD_COUNT)->SetWindowText(str); //显示线程个数
         UpdateData(FALSE);
    }
    return 0;
    }

别忘了添加消息映射:

    ON_MESSAGE(WM_SHOW_THREADCOUNT, OnMyMsg)

(3)切换到资源视图,打开对话框编辑器,双击“暂停”按钮,为其添加事件处理函数,代码如下:

    void CTestDlg::OnBnClickedButton2()
    {
    // TODO:  在此添加控件通知处理程序代码
    if (ghThread1)
         SuspendThread((HANDLE)ghThread1); //用API函数暂停线程的执行
    }

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

    void CTestDlg::OnBnClickedButton3()
    {
    // TODO:  在此添加控件通知处理程序代码
    if (ghThread1)
         ResumeThread((HANDLE)ghThread1); //用API函数恢复线程的执行
    }

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

    void CTestDlg::OnBnClickedButton4()
    {
    // TODO:  在此添加控件通知处理程序代码
    if (!ghThread1)
         return;

    if (!g_bRun)
         return; //如果已经结束就直接返回
    g_bRun = false; //设置循环结束变量

    WaitForSingleObject(ghEvent, INFINITE); //无限等待事件有信号
    CloseHandle(ghEvent); //关闭事件句柄
    ghEvent = NULL;
    GetDlgItem(IDC_BUTTON1)->EnableWindow();//设置“开启线程”按钮可用
    }

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

图3-19

【例3.17】利用互斥对象同步_beginthread创建的线程

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

(2)打开Test.cpp,在其中输入如下代码:

    #include "stdafx.h"
    #include "windows.h"
    #include "process.h"
    #include <clocale>

    int gticketId = 10;  //记录卖出的车票号
    CCriticalSection gcs; // 定义CCriticalSection对象

    void threadfunc(LPVOID param)
    {
    TCHAR chWin;

    if (param == 0) chWin = _T('甲'); //甲窗口
    else chWin = _T('乙'); //乙窗口
    while (1)
    {
         gcs.
         if (gticketId <= 0) //如果车票全部卖出了,则退出循环
         {
              ReleaseMutex(ghMutex); //释放互斥对象所有权
              break;
         }
         setlocale(LC_ALL, "chs"); //为控制台设置中文环境
         _tprintf(_T("%c窗口卖出的车票号 = %d\n"), chWin, gticketId); //打印信息
         gticketId--;//车票减少一张
         ReleaseMutex(ghMutex); //释放互斥对象所有权
     }
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
    int i;
    uintptr_t h[2];

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

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

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

图3-20

【例3.18_beginthreadex函数的简单示例

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

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

    #include "pch.h"
    #include <tchar.h>
    #include <windows.h>
    #include <stdio.h>
    #include <process.h>

    unsigned gCounter;
    unsigned __stdcall ThreadFunc(void* pArguments)
    {
    while (gCounter < 500000) //不断循环累加
         gCounter++;
    printf("子线程运行结果:%d\n", gCounter);
    return 0;
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
    HANDLE hThread;
    unsigned threadID;

    //创建一个子线程
    hThread = (HANDLE)_beginthreadex(NULL, 0, &ThreadFunc, NULL, 0, &threadID);
    WaitForSingleObject(hThread, INFINITE); //等待子线程结束
    printf("子线程运行结果应该是 500000;实际结果是%d\n", gCounter); //打印结果
    CloseHandle(hThread); //关闭线程句柄,销毁线程对象
    return 0;
    }

_beginthreadex创建的线程可以使用WaitForSingleObject函数来等待子线程句柄hThread的方式判断子线程释放结束,并且要显式地关闭子线程句柄。

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

图3-21