最近工作中要維護一個windows模塊,用到了mfc中的CEvent類。這算是很久很久以前的老朋友了吧,估計和我超過10年沒見過面了,不過工作就是工作,技術上來不得半點含糊,所以還是重新認識一下這位老朋友吧。
本文用一個具體的例子來對CEvent類進行介紹,基本上掌握了這個例子后,我們就算是徹底認識CEvent類了。其實其它windows多線程同步的內核對象也大體如此,這是一幫老朋友們。
1.CEvent類
CEvent的接口很少:
基類就更簡單了:
其實CEvent類只是對原生的Windows API的一層很淺的封裝。這可以從它的構造函數源代碼中輕易的看出來:
CEvent::CEvent(BOOL bInitiallyOwn, BOOL bManualReset, LPCTSTR pstrName, LPSECURITY_ATTRIBUTES lpsaAttribute) : CSyncObject(pstrName) { m_hObject = ::CreateEvent(lpsaAttribute, bManualReset, bInitiallyOwn, pstrName); if (m_hObject == NULL) AfxThrowResourceException(); }
2.測試程序
既然要用MFC,測試用例當然是帶界面的了:
如果有足夠老的程序員應該對這個界面不會陌生,它和侯捷的那本《win32多線程程序設計》中的CEvent例子很象。這本書實在是太老了,侯捷的代碼寫得也談不上漂亮,所以我干脆動手重新擼了一個。
測試程序演示了CEvent的兩種模式:自動模式和手動模式,並分別對幾個類方法進行了測試。
這是一個標准的MFC對話框程序,開發工具用VS2017:
怎么通過向導建立工程?怎么在資源里拖放控件?怎么建立消息映射等等太簡單了,我就跳過了,下面將主要講解主窗口的CEventDemoDlg類。
3.准備工作
在本測試程序中,界面的開發是次要的,主要是多線程的開發,下面進行一些准備工作。
核心對象的創建和銷毀:
void CEventDemoDlg::InitEvent(BOOL bManualReset) { m_event = new CEvent(FALSE, bManualReset, _T("EventDemoEvent")); } void CEventDemoDlg::ExitEvent() { if (m_event != NULL) { delete m_event; } }
再給個公共的訪問方法:
CEvent* event()
{
return m_event;
}
工作線程:
先簡單設計一下工作線程的持有數據:
struct ThreadData { int id; CEventDemoDlg* dialog; CWinThread* thread; };
id用於標識線程;dialog記錄各個線程的訪問資源;thread主要是為了處理線程的退出。
然后是工作線程:
UINT AFX_CDECL workThread(LPVOID lpParam) { CEventDemoDlg::ThreadData* threadData = (CEventDemoDlg::ThreadData*)lpParam; int id = threadData->id; CEventDemoDlg* dialog = threadData->dialog; while (true) { DWORD ret = WaitForSingleObject(dialog->event()->m_hObject, INFINITE); if (dialog->isExitThread()) { break; } CString message; message.Format(_T("thread %d write %d"), id, ret); dialog->SendMessage(WM_CUSTUM_WRITE_RESULT, (WPARAM)message.AllocSysString(), 0); Sleep(200); } return 0; }
工作線程蠻簡單,主要是等待核心對象,等到后就發送一個消息到主對話框。注意這里發送的消息內容應該用AllocSysString在堆中分配,因為工作線程本身一跑起來就如脫韁的野馬,並不適合持有消息內容。
關於自定義windows消息和消息內容的界面顯示,都沒啥難度:
#define WM_CUSTUM_WRITE_RESULT WM_APP + 100 ON_MESSAGE(WM_CUSTUM_WRITE_RESULT, &CEventDemoDlg::OnWriteResult) LRESULT CEventDemoDlg::OnWriteResult(WPARAM wParam, LPARAM lParam) { BSTR param = (BSTR)wParam; CString message(param); SysFreeString(param); m_result.AddString(message); int count = m_result.GetCount(); if (count > 0) { m_result.SetCurSel(count - 1); } return 0; }
我們想讓工作線程可以優雅的退出,所以這里加了一個isExitThread標記。
工作線程的創建:
void CEventDemoDlg::InitThread() { m_isExitThread = false; for (int i = 0; i < 3; i++) { ThreadData* threadData = new ThreadData; m_threadDatas.push_back(threadData); threadData->id = i; threadData->dialog = this; threadData->thread = AfxBeginThread(workThread, (LPVOID)threadData, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); threadData->thread->ResumeThread(); } }
這里暫定3個工作線程,實際線程個數應該根據業務來,或許還要定義一個函數,這里簡化了,就直接用這個魔數吧。
因為工作線程個數實際上並不固定,所以相應的ThreadData也是動態分配的。
工作線程的銷毀:
void CEventDemoDlg::ExitThread() { m_isExitThread = true; size_t count = m_threadDatas.size(); HANDLE* threads = new HANDLE[count]; for (size_t i = 0; i < count; i++) { ThreadData* threadData = m_threadDatas[i]; m_event->SetEvent(); threads[i] = threadData->thread->m_hThread; } WaitForMultipleObjects(DWORD(count), threads, TRUE, INFINITE); delete[] threads; for (size_t i = 0; i < count; i++) { ThreadData* threadData = m_threadDatas[i]; delete threadData; } m_threadDatas.clear(); }
首先設置了m_isExitThread退出標記,但是千萬別以為設置了這個標記工作線程就會真的退出,那就大錯特錯了,因為工作線程可能正處在等待的假死狀態,是不會進行標記判斷的。所以下一步要循環挨個喚醒這幫家伙,這樣它們就能優雅的退出了。
實際線程退出的時間是無法確定的,所以這里用WaitForMultipleObjects來進行多個核心對象的等待,以確保這幫慢騰騰的老家伙確實是優雅的落幕了。
最后清除線程的持有數據。
4.自動模式
我們可以把CEvent比喻成一道食堂的大門,工作線程比喻成打飯的程序員。那么SetEvent就是開門,可以打飯;ResetEvent就是關門,不可以打飯。
那么什么是自動模式呢?你可以理解成這是一道帶電子鎖的智能大門,所謂的自動的意思就是它打開后會立即自動關門。
初始化環境,在對話框的OnInitDialog中我們有下面的初始化處理:
CButton* radio = (CButton*)GetDlgItem(IDC_RADIO_AUTOMATIC); radio->SetCheck(TRUE); InitEvent(FALSE); InitThread();
在界面上打上自動模式的標記。
初始化核心對象,這里的FALSE表示是自動模式。此時門是關着的。
初始化工作線程。這些家伙都在門口等着吃飯。
下面是開門:
void CEventDemoDlg::OnBnClickedButtonSetEvent() { // TODO: 在此添加控件通知處理程序代碼 m_event->SetEvent(); }
沒錯,一次就放進來一個人吃飯。因為是自動模式,開門后剛進來一個人,門就自動關上了。
點擊三次后:
點了三次才進來三個人,就是這么費勁。所以,你大可把自動模式想象成曾今的國營飯店,一次只能服務一桌客人。
關門,點擊ResetEvent:
void CEventDemoDlg::OnBnClickedButtonResetEvent() { // TODO: 在此添加控件通知處理程序代碼 m_event->ResetEvent(); }
沒有任何反應。因為自動模式是自帶關門功能的。
點擊PulseEvent:
void CEventDemoDlg::OnBnClickedButtonPulseEvent() { // TODO: 在此添加控件通知處理程序代碼 m_event->PulseEvent(); }
pulse是脈沖的意思,這代表一次放進去一波客人,不過在自動模式下,因為門關得太快,一次也只能一個客人。所以這個效果和點擊SetEvent是一樣的。
點擊PulseEvent三次后:
5.手動模式
在界面上切換到手動模式:
void CEventDemoDlg::OnBnClickedRadioManual() { // TODO: 在此添加控件通知處理程序代碼 ExitThread(); ExitEvent(); InitEvent(TRUE); InitThread(); }
首先銷毀了工作線程,銷毀了核心對象。然后重建新的核心對象和工作線程。
這里的TRUE表示是手動模式。此時門是關着的。
初始化工作線程。這些家伙都在門口等着吃飯。
下面點擊SetEvent開門:
void CEventDemoDlg::OnBnClickedButtonSetEvent() { // TODO: 在此添加控件通知處理程序代碼 m_event->SetEvent(); }
大門一開,工作線程們果然如脫韁的野馬般跑個不停。
趕緊點擊關門:
void CEventDemoDlg::OnBnClickedButtonResetEvent() { // TODO: 在此添加控件通知處理程序代碼 m_event->ResetEvent(); }
工作線程們終於停下來了。
點擊Clear Result清理一下狼藉的現場。
void CEventDemoDlg::OnBnClickedButtonClearResult() { // TODO: 在此添加控件通知處理程序代碼 m_result.ResetContent(); }
這次試一試點擊PulseEvent:
void CEventDemoDlg::OnBnClickedButtonPulseEvent() { // TODO: 在此添加控件通知處理程序代碼 m_event->PulseEvent(); }
這就是脈沖的意思,一次將門口正在等待的一波工作線程統統放進來,然后關門。
5.后記
CEvent是Windows系統特有的一種線程同步的核心對象,個人感覺設計得有些復雜了。但不可否認,正是因為它的多面性,在實際開發中,它的出場幾率可是相當高的。能把這個同步的核心對象用好的程序員,其它的幾個同步的核心對象就通通不在話下了。