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