基于MFC的多线程编程 【转贴】

来源:互联网 发布:软件架构总结 编辑:程序博客网 时间:2024/06/06 00:49

  在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,

   pRecalcInfo,

   pRecalcInfo->m_nRecalcSpeedSeconds,

   pRecalcInfo->m_hwndNotifyProgress);

   SetEvent(pRecalcInfo->m_hEventRecalcDone);

   if (!bRecalcCompleted) // If interrupted by kill then...

   break; // terminate this thread by exiting the proc.

   ::PostMessage(pRecalcInfo->m_hwndNotifyRecalcDone, WM_USER_RECALC_DONE, 0, 0);

   }

   if (!bRecalcCompleted)

   SetEvent(pRecalcInfo->m_hEventRecalcThreadKilled);

   return 0;

   }

  

  BOOL SlowAdd(int nInt1, int nInt2, int& nResult, CRecalcThreadInfo* pInfo, int nSeconds,HWND hwndNotifyProgress)

  {

   CWnd* pWndNotifyProgress = CWnd::FromHandle(hwndNotifyProgress);

   BOOL bRestartCalculation = TRUE;

   while (bRestartCalculation)

   {

   bRestartCalculation = FALSE;

   for (int nCount = 1; nCount <20; nCount++)

   {

   if (pInfo != NULL

   && WaitForSingleObject(pInfo->m_hEventKillRecalcThread, 0) == WAIT_OBJECT_0)

   {

   if (hwndNotifyProgress != NULL)

   {

   pWndNotifyProgress->PostMessage( WM_USER_RECALC_IN_PROGRESS);

   }

   return FALSE; // Terminate this recalculation

   }

   if (pInfo != NULL

   &&WaitForSingleObject(pInfo->m_hEventStartRecalc, 0) == WAIT_OBJECT_0)

   {

   nInt1 = pInfo->m_nInt1;

   nInt2 = pInfo->m_nInt2;

   bRestartCalculation = TRUE;

   continue;

   }

   // update the progress indicator

   Sleep(nSeconds * 50);

   }

   // update the progress indicator

   }

   nResult = nInt1 + nInt2;

   return TRUE;

  }

  上面的代码充分显示了几个事件对象的用法。当线程刚启动时,先等待m_hEventStartRecalc的状态为允许,然后检查m_hEventKillRecalcThread事件对象的状态。注意这两个等待函数调用的第二个参数的区别:在进入计算函数之前,设置m_hEventRecalcDone事件为不允许状态;待计算结束后,将其设置为允许状态。在计算函数的处理过程中,循环检查事件m_hEventKillRecalcThread和m_hEventStartRecalc的状态,如果m_hEventKillRecalcThread事件允许,则退出线程,中止计算。

  当计算线程在计算时,主线程可继续接受用户输入(包括菜单选择)。用户可通过菜单项中止计算线程。中止线程的处理比较简单,代码如下:

  void CRecalcDoc::OnKillWorkerThread()

  {

   SetEvent(m_hEventKillRecalcThread);

   SetEvent(m_hEventStartRecalc);

   WaitForSingleObject(m_hEventRecalcThreadKilled, INFINITE);

   m_pRecalcWorkerThread = NULL;

   m_bRecalcInProgress = FALSE; // but m_bRecalcNeeded is still TRUE

   UpdateAllViews(NULL, UPDATE_HINT_SUM);

  }

  通过设置m_hEventKillRecalcThread事件对象,计算线程的循环就会检测到该事件的状态,最终引起线程的退出。注意:线程的中止因函数的退出而自然中止,而没有用强行办法中止,这样可保证系统的安全性。另外,在程序的很多地方使用了PostMessage来更新计算进度的指示,使用PostMessage函数发送消息可立即返回,无需等待,这样就避免了阻塞,比较符合多线程编程的思想,建议读者使用这种消息发送方法。尤其是在多个UI线程编程时,用这种方法更合适。

  (2)MtMDI

   例程MtMDI是一个MDI应用,每一个子窗口是一个用户接口线程,子窗口里有一个来回弹跳的小球,小球的运动由计时器控制,此处不加以讨论。下面,我们来看看UI线程的创建过程以及它与MDI的结合。

  通过菜单命令New Bounce,可在主框架窗口类中响应菜单命令,函数代码如下:

  void CMainFrame::OnBounce()

  {

   CBounceMDIChildWnd *pBounceMDIChildWnd = new CBounceMDIChildWnd;

   if (!pBounceMDIChildWnd->Create( _T("Bounce"),

   WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, rectDefault, this))

   return;

  }

  函数调用子框架窗口的创建函数,代码如下:

  BOOL CBounceMDIChildWnd::Create(LPCTSTR szTitle, LONG style, const RECT& rect, CMDIFrameWnd* parent)

  {

   // Setup the shared menu

   if (menu.m_hMenu == NULL)

   menu.LoadMenu(IDR_BOUNCE);

   m_hMenuShared = menu.m_hMenu;

   if (!CMDIChildWnd::Create(NULL, szTitle, style, rect, parent))

   return FALSE;

   CBounceThread* pBounceThread = new CBounceThread(m_hWnd);

   pBounceThread->CreateThread();

   return TRUE;

  }

  当CBounceMDIChildWnd子窗口被删除时,Windows会同时删除CBounceWnd窗口(内嵌在线程对象pBounceThread中),因为它是CBounceMDIChildWnd的子窗口。由于CBounceWnd运行在单独的线程中,故当CBounceWnd子窗口被删除时,CWinThread线程对象也会自动被删除。

  上述函数生成一个新的UI线程对象pBounceThread,并调用CreateThread函数创建线程。至此,线程已被创建,但还需要做初始化工作,如下面的函数InitInstance所示:

  int CBounceThread::InitInstance()

  {

   CWnd* pParent = CWnd::FromHandle(m_hwndParent);

   CRect rect;

   pParent->GetClientRect(&rect);

   BOOL bReturn = m_wndBounce.Create(_T("BounceMTChildWnd"),WS_CHILD | WS_VISIBLE, rect, pParent);

   if (bReturn)

   m_pMainWnd = &m_wndBounce;

   return bReturn;

  }

  注意:这里,将m_pMainWnd设置为新创建的CBounceWnd窗口是必需的。只有这样设置了,才能保证当CBounceWnd窗口被删除时,线程会被自动删除。

  (3)Mutexes

  例程Mutexes是一个对话框程序。除主线程外,还有两个线程:一个用于计数,一个用于显示。在本例中,这两个线程都是从CWinThread派生出来的,但并不用于消息循环处理,派生类重载了Run函数,用于完成其计数和显示的任务。

  在对话框类中使用了一个内嵌的CMutex对象。对话框初始化时创建两个线程,并设置相应的参数,然后启动运行两个线程。

  当用户设置了对话框的同步检查框标记后,两个线程的同步处理有效。在计数线程的循环中,先调用CSingleLock::Lock函数,然后进行计数修改,最后调用CSingleLock::Unlock函数。注意:这里的CSingleLock对象根据主对话框的CMutex对象产生。在显示线程的循环中,先调用CSingleLock::Lock函数,然后取到计数值,最后调用CSingleLock::Unlock函数。注意:这里的CSingleLock对象也是由主对话框的CMutex对象产生。类似这种情况:一个线程要读取数据,另一个线程要修改数据,这是我们在处理多线程问题时碰到的最典型的情况。此处采用的方法也具有典型意义。源代码可通过查看例程或通过联机帮助来获取。

  五、结束语

  多线程函数是Win32不同于Win16的一个重要方面,其编程技术较为新颖,在程序设计思路上不同于传统的模块结构化方法,比一般的面向对象的思路也较为复杂,尤其是对于多处理器平台的处理更为复杂。要设计出性能良好的多线程程序,不仅需要对操作系统的处理过程很清楚,还需要对具体应用有一个全面的认识,并对应用中各线程部分的关系非常清楚,对同步模块中的同步对象的具体含义应尽可能地清晰明了,以利于在程序中控制同步事件的发生,避免出现死锁或不能同步处理的现象。

  在其它的开发语言(如Visual Basic 5.0)中也提供了对多线程的支持,但从性能和安全的角度考虑,这种多线程支持受到较多的限制。不过,就一般应用而言,用这种处理方法已经足够了。

  目前,大多数的计算机都是单处理器(CPU)的,在这种机器上运行多线程程序,有时反而会降低系统的性能。如果两个非常活跃的线程为了抢夺对CPU的控制权,则在线程切换时会消耗很多的CPU资源,但对于大部分时间被阻塞的线程(例如等待文件I/O操作),则可用一个单独的线程来完成。这样,就可将CPU时间让出来,使程序获得更好的性能。因此,在设计多线程应用程序时,应慎重选择,并且视具体情况加以处理,使应用程序获得最佳的性能。