这是多线程编程之二-MFC
基于MFC的多线程编程
在Visual C++ 5.0附带的MFC 4.21类库中,也提供了多线程编程的支持,基本原理与上面所讲的基于Win32函数的设计一致,但由于MFC对同步对象作了封装,因此对用户编程实现来说更加方便,避免了对象句柄管理上的繁琐工作。更重要的是,在多个窗口线程情况下,MFC中直接提供了用户接口线程的设计。
在MFC 中,线程分为两种:用户接口线程和辅助线程。用户接口线程常用于接收用户的输入,处理相应的事件和消息。在用户接口线程中,包含一个消息处理循环,其中 CWinApp就是一个典型的例子,它从CWinThread派生出来,负责处理用户输入产生的事件和消息。辅助线程常用于任务处理(比如计算)不要求用户输入,对用户而言,它在后台运行。Win32 API并不区分这两种线程的类型,它只是获取线程的起始地址,然后开始执行线程。而MFC则针对不同的用户需要作了分类。如果我们需要编写多个有用户接口的线程的应用程序,则利用Win32 API要写很多的框架代码来完成每个线程的消息事件的处理,而用MFC则可以充分发挥MFC中类的强大功能,还可以使用ClassWizard来帮助管理类的消息映射和成员变量等,我们就可以把精力集中到应用程序的相关代码编写上。
辅助线程编程较为简单,设计的思路与上节所讲的基本一致:一个基本函数代表了一个线程,创建并启动线程后,则线程进入运行状态;如果线程用到共享资源,则需要进行资源同步处理。共享资源的同步处理在两种线程模式下完全一致。
我们知道:基于MFC的应用程序有一个应用对象,它是CWinApp派生类的对象,该对象代表了应用进程的主线程。当线程执行完(通常是接收到WM_QUIT消息)并退出线程时,由于进程中没有其它线程的存在,故进程也自动结束。类CWinApp从 CWinThread派生出来,CWinThread是用户接口线程的基本类。我们在编写用户接口线程时,需要从CWinThread派生我们自己的线程类,ClassWizard可以帮助我们完成这个工作。
下面列出编写用户接口线程的基本步骤。
1.用 ClassWizard派生一个新的类,设置基类为CWinThread
注意:类的DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE宏是必需的,因为创建线程时需要动态创建类的对象。根据需要可将初始化和结束代码分别放到类的 InitInstance和ExitInstance函数中。如果需要创建窗口,则可在InitInstance函数中完成。
2.创建线程并启动线程
可以用两种方法来创建用户接口线程。
(1)MFC提供了两个版本的 AfxBeginThread函数,其中一个用于创建用户接口线程,函数原型如下:
CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority, UINT nStackSize , DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs );
其中,参数pThreadClass指定线程的运行类,函数返回线程对象。
在创建线程时,可以指定线程先挂起,将参数dwCreateFlags设置为CREATE_SUSPENDED。然后,做一些初试工作,如对变量赋值等。最后,再调用线程类的 ResumeThread函数启动线程。
函数AfxBeginThread的另一个版本指定一个线程函数并设置相应的参数,其它设置及用法与上述函数基本相同。
(2)我们也可以不用AfxBeginThread创建线程,而是分两步完成:首先,调用线程类的构造函数创建一个线程对象;其次,调用CWinThread::CreateThread函数来创建该线程。
注意:在这种情况下,在线程类中需要有公有的构造函数以创建其相应的C++对象。
线程建立并启动后,则线程在线程函数执行过程中一直有效。如果是线程对象,则在对象被删除之前,先结束线程。CWinThread已经为我们完成了线程结束的工作。
3. 同步对象的使用
不管是辅助线程还是用户接口线程,在存取共享资源时,都需要保护共享资源,以免引起冲突,造成错误。处理方法类似于Win32 API函数的使用,但MFC为我们提供了几个同步对象C++类,即CSyncObject、CMutex、CSemaphore、CEvent、 CCriticalSection。这里,CSyncObject为其它四个类的基类,后四个类分别对应前面所讲的四个Win32 API同步对象。
通常,我们在C++对象的成员函数中使用共享资源,或者把共享资源封装在C++类的内部。我们可将线程同步操作封装在对象类的实现函数当中,这样在应用中的线程使用C++对象时,就可以像一般对象一样使用它,简化了使用部分代码的编写,这正是面向对象编程的思想。这样编写的类被称作"线程安全类"。在设计线程安全类时,首先应根据具体情况在类中加入一个同步对象类数据成员。然后,在类的成员函数中,凡是所有修改公共数据或者读取公共数据的地方均要加入相应的同步调用。一般的处理步骤是:创建一个CSingleLock或者CMultiLock对象,然后调用其Lock函数。当对象结束时,自动在析构函数中调用Unlock函数,当然也可以在任何希望的地方调用Unlock函数。
如果不是在特定的C++对象中使用共享资源,而是在特定的函数中使用共享资源(这样的函数称为"线程安全函数"),那么还是按照前面介绍的办法去做:先建立同步对象,然后调用等待函数,直到可以访问资源,最后释放对同步对象的控制。
下面我们讨论四个同步对象分别适用的场合:
(1)如果某个线程必须等待某些事件发生后才能存取相应资源,则用CEvent;
(2)如果一个应用同时可以有多个线程存取相应资源,则用CSemaphore;
(3)如果有多个应用(多个进程)同时存取相应资源,则用CMutex,否则用CCriticalSection。
使用线程安全类或者线程安全函数进行编程,比不考虑线程安全的编程要复杂,尤其在进行调试时情况更为复杂,我们必须灵活使用Visual C++提供的调试工具,以保证共享资源的安全存取。线程安全编程的另一缺点是运行效率相对要低些,即使在单个线程运行的情况下也会损失一些效率。所以,我们在实际工作中应具体问题具体分析,以选择合适的编程方法。
4. 多线程编程例程分析
上面讲述了在Visual C++ 5.0中进行多线程编程的技术要点,为了充分说明这种技术,我们来分析一下Visual C++提供的有关多线程的例程,看看一些多线程元素的典型用法。读者可运行这些例程,以获得多线程运行的直观效果。
(1)MtRecalc
例程MtRecalc的功能是在一个窗口中完成简单的加法运算,用户可输入加数和被加数,例程完成两数相加。用户可通过菜单选择单线程或用辅助线程来做加法运算。如果选择辅助线程进行加法运算,则在进行运算的过程中,用户可继续进行一些界面操作,如访问菜单、编辑数值等,甚至可以中止辅助运算线程。为了使其效果更加明显,例程在计算过程中使用了循环和延时,模拟一个复杂的计算过程。
在程序的 CRecalcDoc类中,用到了一个线程对象和四个同步事件对象:
CWinThread* m_pRecalcWorkerThread;
HANDLE m_hEventStartRecalc;
HANDLE m_hEventRecalcDone;
HANDLE m_hEventKillRecalcThread;
HANDLE m_hEventRecalcThreadKilled;
当用户选择了菜单项Worker Thread后,多线程功能才有效。这时,或者选择菜单项Recalculate Now,或者在窗口中的编辑控制转移焦点时,都会调用函数:
void CRecalcDoc::UpdateInt1AndInt2(int n1, int n2, BOOL bForceRecalc);
在多线程的情况下,还会调用下面的CRecalcDoc::RecalcInSecondThread函数:
void CRecalcDoc::RecalcInSecondThread()
{
if (m_pRecalcWorkerThread == NULL)
{
m_pRecalcWorkerThread =
AfxBeginThread(RecalcThreadProc, &m_recalcThreadInfo);
}
m_recalcThreadInfo.m_nInt1 = m_nInt1;
m_recalcThreadInfo.m_nInt2 = m_nInt2;
POSITION pos = GetFirstViewPosition();
CView* pView = GetNextView(pos);
m_recalcThreadInfo.m_hwndNotifyRecalcDone = pView->m_hWnd;
m_recalcThreadInfo.m_hwndNotifyProgress = AfxGetMainWnd()->m_hWnd;
m_recalcThreadInfo.m_nRecalcSpeedSeconds = m_nRecalcSpeedSeconds;
SetEvent(m_hEventRecalcDone);
ResetEvent(m_hEventKillRecalcThread);
ResetEvent(m_hEventRecalcThreadKilled);
SetEvent(m_hEventStartRecalc);
}
上面加粗的语句是与多线程直接相关的代码,应用程序调用AfxBeginThread启动了线程,把m_recalcThreadInfo作为参数传给线程函数。函数中最后的四行语句设置了四个事件对象的状态,这些事件对象在文档类的构造函数中创建。下面是实际的运算线程函数:
UINT RecalcThreadProc(LPVOID pParam)
{
CRecalcThreadInfo* pRecalcInfo = (CRecalcThreadInfo*)pParam;
BOOL bRecalcCompleted;
while (TRUE)
{
bRecalcCompleted = FALSE;
if (WaitForSingleObject(pRecalcInfo->m_hEventStartRecalc, INFINITE)
!= WAIT_OBJECT_0)
break;
if (WaitForSingleObject(pRecalcInfo->m_hEventKillRecalcThread, 0)
WAIT_OBJECT_0)
break; // Terminate this thread by existing the proc.
ResetEvent(pRecalcInfo->m_hEventRecalcDone);
bRecalcCompleted = SlowAdd(pRecalcInfo->m_nInt1,
pRecalcInfo->m_nInt2,
pRecalcInfo->m_nSum,
多线程编程
最新推荐文章于 2023-06-26 22:15:47 发布