![Visual C++ 2017网络编程实战](https://wfqqreader-1252317822.image.myqcloud.com/cover/473/34061473/b_34061473.jpg)
3.4 MFC多线程开发
前面纯粹使用Win32 API函数进行多线程开发,现在我们利用MFC库来进行多线程开发。MFC对多线程的支持是通过对多线程开发相关的Win32 API进行简单的封装后实现的。
在MFC中,用类CWinThread的对象来表示一个线程,比如每个MFC程序的主线程都有一个继承自CWinApp的应用程序类,而CWinApp继承自CWinThread。类CWinThread支持两种线程类型:工作者线程和用户界面线程。工作者线程没有收发消息的功能,通常用于后台计算工作,比如耗时的计算过程、打印机的后台打印等;用户界面线程具有消息队列和消息循环,可以收发消息,一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。
类CWinThread的成员中不但包含了控制线程的相关成员函数(比如暂停和恢复),而且包括线程的ID和句柄,主要成员可以见表3-3。
表3-3 类CWinThread的成员
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-T122_76820.jpg?sign=1738971374-A23V17TlyJmNSqSCF7CuFfslHRZP2y4q-0-ed09f4c41626d7b3416e97419db03497)
3.4.1 线程的创建
在MFC中有两种方式可以创建线程:一种是调用MFC库中的全局函数AfxBeginThread;另一种是先定义CWinThread对象,然后调用成员函数CWinThread::CreateThread来创建线程。
函数AfxBeginThread是MFC库中的全局函数,不是Win32 API函数,只能在MFC程序中使用。该函数创建并启动一个线程,有两种重载形式,分别用于创建工作者线程(辅助线程)和用户界面线程(UI线程)。创建工作者线程的函数形式如下:
CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL,UINT nStackSize = 0, DWORD dwCreateFlags = 0,LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
其中,pfnThreadProc为工作线程的线程函数地址。工作线程的线程函数形式如下:
UINT __cdecl MyFunction( LPVOID pParam );
需要注意的是,该线程函数的返回值类型是UINT,并且函数调用约定为__cdecl,而不是WINAPI,前面CreateThread创建的线程函数的返回值类型为DWORD,调用约定为WINAPI,即__stdcall。其中,pParam为传给线程函数的参数;nPriority为线程的优先级,如果为0,即宏THREAD_PRIORITY_NORMAL,则线程与其父线程具有相同的优先级;nStackSize表示线程为自己分配的堆栈的大小,其单位为字节,如果该参数为0,则线程的堆栈被设置成与父线程堆栈相同大小;dwCreateFlags用来确定线程在创建后释放立即开始执行,如果为0则线程在创建后立即执行,如果为CREATE_SUSPEND,则线程在创建后立刻被挂起;lpSecurityAttrs表示线程的安全属性指针,一般为NULL。当函数成功时返回CWinThread对象的指针,如果失败就返回NULL。
用户界面线程也可以用AfxBeginThread创建,注意不同的是第一个参数。创建用户界面线程的AfxBeginThread函数形式如下:
CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL,UINT nStackSize = 0,DWORD dwCreateFlags = 0,LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
其中,参数pThreadClass指向从CWinThread派生的子类对象的RUNTIME_CLASS,RUNTIME_CLASS可以从一个C++类名获得运行时的类结构;其他参数和函数返回值与前面介绍的相同,不再赘述。
用户界面线程通常用于处理用户输入和响应用户事件,这些行为独立于该应用程序的其他线程。用户界面线程必须包含有消息循环,以便可以处理用户消息。创建用户界面线程时,必须首先从 CWinThread派生类,而且必须要重写类的InitInstance函数。
实际上,AfxBeginThread内部会先新建一个CWinThread对象,然后调用CWinThread::CreateThread来创建线程,最后AfxBeginThread会返回这个CWinThread对象,如果我们没有把CWinThread::m_bAutoDelete设为FALSE,则当线程函数返回的时候会自动删除这个CWinThread对象。因此,注意不要等线程结束的时候去关闭线程句柄,因为此时可能CWinThread对象已经销毁了,根本无法引用其成员变量m_hThread(线程句柄)了。比如:
CWinThread *pwinthread1 pwinthread1 = AfxBeginThread(threadfunc, (LPVOID)0); WaitForSingleObject(pwinthread1->m_hThread, INFINITE); //等待线程结束 CloseHandle(pwinthread1->m_hThread); //可能已经是无效指针
该段代码在单步调试的时候会报异常错误,因为最后一句中的pwinthread1很可能是无效的。既然删除了CWinThread对象,那么我们就不必去关闭线程句柄了。
此外,如果我们把CWinThread::m_bAutoDelete设为TRUE,那么最后要自己去删除CWinThread对象(比如delete pwinthread1;),否则会造成内存泄漏。
CWinThread::CreateThread内部是通过_beginthreadex函数来创建线程的。只不过AfxBeginThread和CWinThread::CreateThread做了更多的初始化和检查工作。在AfxBeginThread创建的线程中使用CRT库函数是安全的。
下面我们创建一个用户界面线程,在用户界面线程中会创建一个窗口,并且点击窗口的时候会出现一个信息框。
【例3.19】AfxBeginThread创建用户界面线程
(1)新建一个单文档工程。
(2)切换到类视图,添加一个MFC类CMyThread(继承于CWinThread),作以为用户界面类;然添加一个MFC类CMyWnd(继承于CFrameWnd),用于在界面线程中创建窗口。
(3)打开MyWnd.h,把CMyWnd构造函数的访问属性改为public,同时添加一个进度条变量:
public: CProgressCtrl m_pos; //进度条控件变量 CMyWnd(); //构造函数
为CMyWnd添加WM_CREATE的消息处理函数OnCreate。在该函数中我们创建一个进度条控件并设置计时器,代码如下:
int CMyWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { int i; if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; // TODO: 在此添加您专用的创建代码 //创建进度条 m_pos.Create(WS_CHILD | WS_VISIBLE, CRect(10, 10, 300, 50), this, 10001); m_pos.SetRange(0, 100); //设置范围 m_pos.SetStep(1); //设置步长 SetTimer(1, 50, NULL); //开启计时器,时间间隔为50ms return 0; }
为CMyWnd添加计时器消息WM_TIMER的消息处理函数OnTimer,在其中我们让计时器向前走一步,代码如下:
void CMyWnd::OnTimer(UINT_PTR nIDEvent) { // TODO: 在此添加消息处理程序代码和/或调用默认值 m_pos.StepIt(); //进度条向前走一步 CFrameWnd::OnTimer(nIDEvent); }
最后为CMyWnd添加窗口销毁消息WM_DESTROY的消息处理函数OnDestroy,在其中我们销毁计时器,代码如下:
void CMyWnd::OnDestroy() { CFrameWnd::OnDestroy(); // TODO: 在此处添加消息处理程序代码 KillTimer(1); //销毁计时器 }
好了,我们在线程中创建的窗口完成了,该窗口运行的时候会不停让进度条往前滚动。
(4)打开MyThread.cpp,找到函数CMyThread::InitInstance,我们在其中添加创建上述窗口的代码:
BOOL CMyThread::InitInstance() { // TODO: 在此执行任意逐线程初始化 CMyWnd *pFrameWnd = new CMyWnd(); //分配空间 pFrameWnd->Create(NULL, _T("线程中创建的窗口" )); //创建窗口 pFrameWnd->ShowWindow(SW_SHOW); //显示窗口 pFrameWnd->UpdateWindow(); return TRUE; }
虽然我们用new分配了一个窗口的堆空间,但是不要用delete去删除它,因为在窗口销毁的时候,系统会自动删除这个C++对象。最后在该文件开头包含头文件MyWnd.h。
(5)切换到资源视图,打开菜单设计器,然后在“视图”菜单下添加一个菜单项“创建用户界面线程”,并为其添加视图类CTestView的事件处理函数,在其中我们将开启一个界面线程,代码如下:
void CTestView::On32771() { // TODO: 在此添加命令处理程序代码 AfxBeginThread(RUNTIME_CLASS(CMyThread)); //创建界面线程 }
类CMyThread就是我们上面创建的界面线程类,最后在文件开头包含头文件MyThread.h。
(6)保存工程并运行,运行结果如图3-22所示。因为这两个窗口是在不同线程中创建的,所以在任务栏里会出现这两个窗口,它们是相互独立的。需要注意的是,如果直接关闭主线程中的窗口,就会导致子线程直接关闭,子线程窗口的销毁动作得不到执行(大家可以CMyWnd::OnDestroy中显示一个信息框来验证),从而造成内存泄漏,所以应该先关闭子线程窗口再关闭主线程窗口。
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-P126_14787.jpg?sign=1738971374-Isto9m0vNN0ytkNqwsXiHAEyT6SHJCws-0-57860381ca43749ef77e606881209f12)
图3-22
3.4.2 线程同步
我们知道,线程同步可以通过同步对象来实现,前面章节介绍了直接用Win32 API进行线程同步。在MFC中,对同步对象进行了C++封装,各个同步函数称为了C++类的成员函数。在MFC中,用于线程同步的类有CCriticalSection(临界区类)、互斥类(CMutex)、事件类(CEvent)和信号量类(CSemaphore),这些类都从同步对象类CSyncObject派生。我们来看一下类CSyncObject在afxmt.h中的定义:
class CSyncObject : public CObject { DECLARE_DYNAMIC(CSyncObject) // Constructor public: explicit CSyncObject(LPCTSTR pstrName); // Attributes public: operator HANDLE() const; HANDLE m_hObject; // Operations virtual BOOL Lock(DWORD dwTimeout = INFINITE); virtual BOOL Unlock() = 0; virtual BOOL Unlock(LONG /* lCount */, LPLONG /* lpPrevCount=NULL */) { return TRUE; } // Implementation public: virtual ~CSyncObject(); #ifdef _DEBUG CString m_strName; virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif friend class CSingleLock; friend class CMultiLock; };
其中,m_hObject存放同步对象的句柄。函数Lock用于锁定某个同步对象,它在内部只是简单地调用等待函数WaitForSingleObject。Unlock是一个纯虚函数,因此类CSyncObject是一个纯虚类,所以该类不应该直接用在程序中,而应该使用它的子类。另外,在末尾有两个友元类CSingleLock和CMultiLock,这两个类没有父类也没有子类,主要用于对共享资源的访问控制。要使用4大同步类(CCriticalSection、CMutex、CEvent、 CSemaphore)来同步线程,必须要使用CSingleLock或CMultiLock来等待或释放同步对象。当一次只需要等待一个同步对象时,使用类CSingleLock;当一次要等待多个同步对象时,使用类CMultiLock。
类CSingleLock的常见成员见表3-4。
表3-4 类CSingleLock的常见成员
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-T127_76851.jpg?sign=1738971374-Y0R3DgpxA4alsfhUwhiopSqwjFY0gopR-0-c38e46f14d4f8ab7f327ef45275a5975)
(1)构造函数CSingleLock的声明如下:
CSingleLock( CSyncObject* pObject, BOOL bInitialLock = FALSE );
其中,参数pObject为指向同步对象的指针,不可以为NULL;bInitialLock表明该同步对象在初始的时候是否锁定同步对象。
(2)函数Lock的声明如下:
BOOL Lock(DWORD dwTimeOut = INFINITE );
其中,参数dwTimeOut为等待同步对象变为可用(有信号状态)所用的时间,单位是毫秒,如果为INFINITE,则函数一直等到同步对象有信号为止。如果函数成功就返回非零,否则返回零。
通常当同步对象变为有信号时,Lock函数将成功返回,同时线程将拥有该同步对象。如果同步对象处于无信号状态(不可用),那么Lock等待dwTimeOut毫秒或一直等下去,直到同步对象有信号。等待dwTimeOut毫秒时,若等待超时,则Lock返回零。
(3)函数Unlock用于释放某个同步对象,声明如下:
BOOL Unlock();
如何函数成功就返回非零,否则返回零。
(4)函数IsLocked判断同步对象释放处于锁定状态,声明如下:
BOOL IsLocked( );
如果同步对象被锁定,函数返回非零,否则返回零。
在使用CSingleLock进行线程同步的时候,不要在多个线程中共享一个CSingleLock对象,通常在一个线程中定义一个对象。比如:
UINT threadfunc() //线程函数 { // gCritSection是类CCriticalSection的全局对象 CSingleLock singleLock(&gCritSection); singleLock.Lock(); // 试图对共享资源进行上锁 if (singleLock.IsLocked()) // 判断资源释放上锁 { // 使用共享资源 // singleLock.Unlock(); //使用完毕后解锁 } }
3.4.2.1 临界区类
类CCriticalSection对临界区对象的操作进行了C++封装。关于临界区的概念前面已经介绍过,这里不再赘述。类CCriticalSection的常见成员函数见表3-5。
表3-5 CCriticalSection的常见成员函数
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-T128_76863.jpg?sign=1738971374-QDYff6SbHZEYF2mfePrm3zEERS7UiF9o-0-57788585af5e83da9c16fa2671750ab1)
类CCriticalSection的用法有两种:一种是单独使用,另一种是和CSingleLock或CMultiLock联合使用。
·单独使用CCriticalSection时,首先创建一个CCriticalSection对象,然后在需要访问临界区时先调用CCriticalSection::Lock函数进行锁定,即获得临界区对象的访问权,然后开始执行临界区代码,在执行完临界区后再调用CCriticalSection:: UnLock函数释放临界区对象。
·第二种方法先定义一个CSingleLock对象,并把CCriticalSection对象的指针作为参数传入其构造函数。然后在需要访问临界区的地方调用函数CSingleLock::Lock,用完临界区后再调用函数CSingleLock:: Unlock,比如:
UINT threadfunc() { // m_CritSection是类CCriticalSection的对象 CSingleLock singleLock(&m_CritSection); singleLock.Lock(); // 试图对共享资源进行上锁 if (singleLock.IsLocked()) // 判断资源释放上锁 { // 使用共享资源 // singleLock.Unlock(); //使用完毕后解锁 } }
下面我们来演示一下这两种用法。同前面Win32 API线程同步一样,我们也来对例3.11进行改造。
【例3.20】单独使用CCriticalSection对象来同步线程
(1)新建一个控制台工程,并在向导的“应用程序设置”界面中勾选“MFC”复选框,这是因为CCriticalSection属于MFC类,如图3-23所示。
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-P129_15320.jpg?sign=1738971374-CSu0Gw4VlaC1TFuhGaArxVC2tc2znhwz-0-1f616d4030282a32458241a86758ace1)
图3-23
(2)在Test.cpp中输入如下代码:
// Test.cpp : 定义控制台应用程序的入口点 #include "stdafx.h" #include "Test.h" #include "afxmt.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // 唯一的应用程序对象 CWinApp theApp; using namespace std; int gticketId = 10; //记录卖出的车票号 CCriticalSection gcs; // 定义CCriticalSection对象 UINT threadfunc(LPVOID param) { TCHAR chWin; if (param == 0) chWin = _T('甲'); //甲窗口 else chWin = _T('乙'); //乙窗口 while (1) { gcs.Lock(); if (gticketId <= 0) //如果车票全部卖出了,则退出循环 { gcs.Unlock(); break; } setlocale(LC_ALL, "chs"); //为控制台设置中文环境 _tprintf(_T("%c窗口卖出的车票号 = %d\n"), chWin, gticketId); //打印信息 gticketId--;//车票减少一张 gcs.Unlock(); //释放临界区对象所有权 Sleep(1); //让出CPU让其他线程有机会执行 } return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0; CWinThread *pwinthread1, *pwinthread2; HMODULE hModule = ::GetModuleHandle(NULL); if (hModule != NULL) { // 初始化 MFC 并在失败时显示错误 if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0)) { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: MFC 初始化失败\n")); nRetCode = 1; } else { // TODO: 在此处为应用程序的行为编写代码 puts("利用CCriticalSection同步线程"); //创建第一个卖票线程 pwinthread1 = AfxBeginThread(threadfunc, (LPVOID)0); //创建第二个卖票线程 pwinthread2 = AfxBeginThread(threadfunc, (LPVOID)1); WaitForSingleObject(pwinthread1->m_hThread, INFINITE); //等待线程结束 //等待线程结束 WaitForSingleObject((HANDLE)pwinthread2->m_hThread, INFINITE); puts("卖票结束"); } } else { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: GetModuleHandle 失败\n")); nRetCode = 1; } return nRetCode; }
程序很简单,首先创建两个工作线程,然后主线程就等待它们执行完毕。在线程函数中,每当要卖票了,就先Lock,卖完票后再Unlock。
(3)保存工程并运行,运行结果如图3-24所示。
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-P131_15784.jpg?sign=1738971374-gL3EhZzvtJ59CJcOYo7DXh4ITaT88nMK-0-5f74f5aad39f234cb160a3d144e5f8b0)
图3-24
【例3.21】联合使用类CCriticalSection和类CSingleLock来同步线程
(1)新建一个控制台工程,并在向导的“应用程序设置”界面中勾选“MFC”复选框。
(2)打开Test.cpp,在其中输入如下代码:
#include "stdafx.h" #include "Test.h" #include "afxmt.h" //线程同步类所需的头文件 #ifdef _DEBUG #define new DEBUG_NEW #endif // 唯一的应用程序对象 CWinApp theApp; using namespace std; int gticketId = 10; //记录卖出的车票号 CCriticalSection gcs; // 定义CCriticalSection对象 UINT threadfunc(LPVOID param) { TCHAR chWin; if (param == 0) chWin = _T('甲'); //甲窗口 else chWin = _T('乙'); //乙窗口 CSingleLock singleLock(&gcs); //定义一个单锁对象,参数为CCriticalSection对象地址 while (1) { singleLock.Lock(); //上锁 if (gticketId <= 0) //如果车票全部卖出了,则退出循环 { singleLock.Unlock(); break; } setlocale(LC_ALL, "chs"); //为控制台设置中文环境 _tprintf(_T("%c窗口卖出的车票号 = %d\n"), chWin, gticketId); //打印信息 gticketId--;//车票减少一张 singleLock.Unlock(); //解锁 Sleep(1); //让出CPU,让其他线程有机会执行 } return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0; CWinThread *pwinthread1, *pwinthread2; HMODULE hModule = ::GetModuleHandle(NULL); if (hModule != NULL) { // 初始化 MFC 并在失败时显示错误 if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0)) { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: MFC 初始化失败\n")); nRetCode = 1; } else { // TODO: 在此处为应用程序的行为编写代码 puts("联合使用类CCriticalSection和类CSingleLock来同步线程"); pwinthread1 = AfxBeginThread(threadfunc, (LPVOID)0); pwinthread2 = AfxBeginThread(threadfunc, (LPVOID)1); WaitForSingleObject(pwinthread1->m_hThread, INFINITE); //等待线程结束 //等待线程结束 WaitForSingleObject((HANDLE)pwinthread2->m_hThread, INFINITE); puts("卖票结束"); } } else { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: GetModuleHandle 失败\n")); nRetCode = 1; } return nRetCode; }
上述代码通过定义CSingleLock局部对象来同步两个线程,也可以定义两个全局的CSingleLock对象,然后根据不同的线程分别使用不同的全局对象,比如线程函数也可以这样写:
CCriticalSection gcs; // 定义CCriticalSection对象 CSingleLock singleLock(&gcs); CSingleLock singleLock2(&gcs); UINT threadfunc(LPVOID param) { TCHAR chWin; if (param == 0) chWin = _T('甲'); //甲窗口 else chWin = _T('乙'); //乙窗口 while (1) { if (param==0) singleLock.Lock(); else singleLock2.Lock(); if (gticketId <= 0) //如果车票全部卖出了,则退出循环 { if (param == 0) singleLock.Unlock(); else singleLock2.Unlock(); break; } setlocale(LC_ALL, "chs"); //为控制台设置中文环境 _tprintf(_T("%c窗口卖出的车票号 = %d\n"), chWin, gticketId); //打印信息 gticketId--;//车票减少一张 if (param == 0) singleLock.Unlock(); else singleLock2.Unlock(); Sleep(1); } return 0; }
两种方式的运行效果相同,但明显第二种方式啰唆了,通常用第一种方式即可。
(3)保存工程并运行,运行结果如图3-25所示。
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-P134_16417.jpg?sign=1738971374-P61N3SeGMDFY6Om7Z2VNeVqN6DT1v9bu-0-5260fad5734218ab353d9eaaed35dd9a)
图3-25
3.4.2.2 互斥类
MFC中的互斥类CMutex封装了利用互斥对象来进行线程同步的操作。互斥类不但能同步一个进程中的线程,还能同步不同进程之间的线程。该类是CSyncObject的子类,继承了Lock函数并重载了Unlock函数,利用这两个函数实现线程同步。
要用互斥类来同步线程也有两种使用方式:一种是CMutex类单独使用,另一种是联合CSingleLock或CMultiLock类一起使用。当在单独使用的时候,先定义一个CMutex对象,然后调用该类的Lock函数来等待互斥对象的所有权,如果等到就开始访问共享资源,访问完毕后再调用该类的Unlock函数来释放互斥对象的所有权。
实际上,CMutex类只是简单地对Win32 API的互斥操作函数进行了封装。比如,CMutex的构造函数中会调用API函数CreateMutex来创建互斥对象并判断释放创建成功。如果要等待互斥对象的所有权,就调用其父类CSyncObject的Lock函数,而CSyncObject::Lock中调用了WaitForSingleObject。该类的Unlock函数重载了父类的Unlock,它的实现如下:
BOOL CMutex::Unlock() { return ::ReleaseMutex(m_hObject); }
实际只是简单地调用了API函数ReleaseMutex来释放互斥对象的所有权。
下面我们单独使用CMutex类来改写例3.11,增加线程的同步功能。
【例3.22】单独使用CMutex类实现线程同步
(1)新建一个控制台工程,并在向导的“应用程序设置”界面中勾选“MFC”复选框。
(2)打开Test.cpp,在其中输入如下代码:
#include "stdafx.h" #include "Test.h" #include "afxmt.h"//线程同步类所需的头文件 #ifdef _DEBUG #define new DEBUG_NEW #endif int gticketId = 10; //记录卖出的车票号 CMutex gmux; // 定义CMutex对象 UINT threadfunc(LPVOID param) { TCHAR chWin; if (param == 0) chWin = _T('甲'); //甲窗口 else chWin = _T('乙'); //乙窗口 while (1) { gmux.Lock(); if (gticketId <= 0) //如果车票全部卖出了,则退出循环 { gmux.Unlock(); break; } setlocale(LC_ALL, "chs"); //为控制台设置中文环境 _tprintf(_T("%c窗口卖出的车票号 = %d\n"), chWin, gticketId); //打印信息 gticketId--;//车票减少一张 gmux.Unlock(); //解锁 Sleep(1); //让出CPU,让其他线程有机会执行 } return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0; CWinThread *pwinthread1, *pwinthread2; HMODULE hModule = ::GetModuleHandle(NULL); if (hModule != NULL) { // 初始化 MFC 并在失败时显示错误 if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0)) { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: MFC 初始化失败\n")); nRetCode = 1; } else { // TODO: 在此处为应用程序的行为编写代码 puts("单独使用类CMutex来同步线程"); pwinthread1 = AfxBeginThread(threadfunc, (LPVOID)0); pwinthread2 = AfxBeginThread(threadfunc, (LPVOID)1); WaitForSingleObject(pwinthread1->m_hThread, INFINITE); //等待线程结束 //等待线程结束 WaitForSingleObject((HANDLE)pwinthread2->m_hThread, INFINITE); puts("卖票结束"); } } else { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: GetModuleHandle 失败\n")); nRetCode = 1; } return nRetCode; }
(3)保存工程并运行,运行结果如图3-26所示。
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-P136_16871.jpg?sign=1738971374-LaPzoUjTTYrg5IHszMNFDakF7SRsXRRB-0-e1210fbaf04c122fd55b80260ca4dd15)
图3-26
3.4.2.3 事件类
MFC中的事件类CEvent封装了利用事件对象来进行线程同步的操作。关于事件对象概念前面我们已经介绍过了,这里不再赘述。其实该类也是对Win32 API事件对象操作进行简单封装。它的常用成员函数如表3-6。
表3-6 CEvent的常用成员函数
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-T137_76895.jpg?sign=1738971374-wyx0Yfise3e2dYlYFShfCyOXtlHjYxeN-0-47891d2968de3d729507bf5a80ee2d77)
如果要等待事件对象变为可用,可以直接使用API函数WaitForSingleObject或者调用其父类的Lock函数(内部也是调用WaitForSingleObject)。
事件类同步线程也有两种方式:一种是单独使用,另一种是联合CSingleLock或CMultiLock来使用。
下面我们用事件类为例3.11增加线程同步功能。
【例3.23】单独使用类CEvent实现线程同步
(1)新建一个控制台工程,并在向导的“应用程序设置”界面中勾选“MFC”复选框。
(2)打开Test.cpp,在其中输入如下代码:
#include "stdafx.h" #include "Test.h" #include "afxmt.h"//线程同步类所需的头文件 #ifdef _DEBUG #define new DEBUG_NEW #endif int gticketId = 10; //记录卖出的车票号 CEvent gEvent; // 定义CEvent对象 UINT threadfunc(LPVOID param) { TCHAR chWin; if (param == 0) chWin = _T('甲'); //甲窗口 else chWin = _T('乙'); //乙窗口 while (1) { gEvent.Lock(); if (gticketId <= 0) //如果车票全部卖出了,则退出循环 { gEvent.SetEvent(); break; } setlocale(LC_ALL, "chs"); //为控制台设置中文环境 _tprintf(_T("%c窗口卖出的车票号 = %d\n"), chWin, gticketId); //打印信息 gticketId--;//车票减少一张 gEvent.SetEvent(); //Sleep(1); //让出CPU,让其他线程有机会执行 } return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0; CWinThread *pwinthread1, *pwinthread2; HMODULE hModule = ::GetModuleHandle(NULL); if (hModule != NULL) { // 初始化 MFC 并在失败时显示错误 if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0)) { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: MFC 初始化失败\n")); nRetCode = 1; } else { // TODO: 在此处为应用程序的行为编写代码 puts("单独使用类CMutex来同步线程"); gEvent.SetEvent(); //设置事件处于有信号状态 pwinthread1 = AfxBeginThread(threadfunc, (LPVOID)0); pwinthread2 = AfxBeginThread(threadfunc, (LPVOID)1); WaitForSingleObject(pwinthread1->m_hThread, INFINITE); //等待线程结束 WaitForSingleObject(pwinthread2->m_hThread, INFINITE); //等待线程结束 puts("卖票结束"); } } else { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: GetModuleHandle 失败\n")); nRetCode = 1; } return nRetCode; }
(3)保存工程并运行,运行结果如图3-27所示。
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-P138_17367.jpg?sign=1738971374-TpEcBMTm9usod6XkOOzWM9xjtOHjuIX1-0-1a26a25c3899a0a3b0c63d17205e417f)
图3-27
3.4.2.4 信号量类
MFC中的信号量类CSemaphore封装了利用信号量对象来进行线程同步的操作。关于信号量概念前面我们已经介绍过了,这里不再赘述。类CSemaphore在构造函数中调用了CreateSemaphore函数来创建信号量对象,并重载了父类的UnLock函数,里面调用了ReleaseSemaphore函数。说到底,也是对Win32 API的信号量对象操作进行了简单封装。等待信号量对象有信号可以用API函数WaitForSingleObject或者调用其父类的Lock函数(内部也是调用WaitForSingleObject)。
信号量类同步线程也有两种方式:一种是单独使用,另一种是联合CSingleLock或CMultiLock来使用。
下面我们用信号量类为例3.11增加线程同步功能。
【例3.24】单独使用类CSemaphore实现线程同步
(1)新建一个控制台工程,并在向导的“应用程序设置”界面中勾选“MFC”复选框。
(2)打开Test.cpp,在其中输入如下代码:
#include "stdafx.h" #include "Test.h" #include "afxmt.h"//线程同步类所需的头文件 #ifdef _DEBUG #define new DEBUG_NEW #endif int gticketId = 10; //记录卖出的车票号 CSemaphore gSp( 1, 50, _T("mySemaphore")); // 定义CSemaphore对象 UINT threadfunc(LPVOID param) { TCHAR chWin; if (param == 0) chWin = _T('甲'); //甲窗口 else chWin = _T('乙'); //乙窗口 while (1) { gSp.Lock(); if (gticketId <= 0) //如果车票全部卖出了,则退出循环 { gSp.Unlock(); break; } setlocale(LC_ALL, "chs"); //为控制台设置中文环境 _tprintf(_T("%c窗口卖出的车票号 = %d\n"), chWin, gticketId); //打印信息 gticketId--;//车票减少一张 gSp.Unlock(); } return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0; CWinThread *pwinthread1, *pwinthread2; HMODULE hModule = ::GetModuleHandle(NULL); if (hModule != NULL) { // 初始化 MFC 并在失败时显示错误 if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0)) { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: MFC 初始化失败\n")); nRetCode = 1; } else { // TODO: 在此处为应用程序的行为编写代码 puts("单独使用类CSemaphore来同步线程"); pwinthread1 = AfxBeginThread(threadfunc, (LPVOID)0); pwinthread2 = AfxBeginThread(threadfunc, (LPVOID)1); WaitForSingleObject(pwinthread1->m_hThread, INFINITE); //等待线程结束 WaitForSingleObject(pwinthread2->m_hThread, INFINITE); //等待线程结束 puts("卖票结束"); } } else { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: GetModuleHandle 失败\n")); nRetCode = 1; } return nRetCode; }
(3)保存工程并运行,运行结果如图3-28所示。
![](https://epubservercos.yuewen.com/F84FA5/18225432201803906/epubprivate/OEBPS/Images/Figure-P140_17782.jpg?sign=1738971374-Vkqx0cLdyT5DwlPx7m1bBYUZq7BOKgbN-0-23db0a1c64ba32f6a4ef840de9207dd1)
图3-28