VC官方多线程例程的分析

我们来分析一下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对象产生。类似这种情况:一个线程要读取数据,另一个线程要修改数据,这是我们在处理多线程问题时碰到的最典型的情况。此处采用的方法也具有典型意义。源代码可通过查看例程或通过联机帮助来获取。这个例程的具体实现参见:http://www.vchome.net/tech/thread_2.htm

你可能感兴趣的:(VC官方多线程例程的分析)