(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