最近工做中要維護一個windows模塊,用到了mfc中的CEvent類。這算是好久好久之前的老朋友了吧,估計和我超過10年沒見過面了,不過工做就是工做,技術上來不得半點含糊,因此仍是從新認識一下這位老朋友吧。程序員
本文用一個具體的例子來對CEvent類進行介紹,基本上掌握了這個例子後,咱們就算是完全認識CEvent類了。其實其它windows多線程同步的內核對象也大致如此,這是一幫老朋友們。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,測試用例固然是帶界面的了:this
若是有足夠老的程序員應該對這個界面不會陌生,它和侯捷的那本《win32多線程程序設計》中的CEvent例子很象。這本書實在是太老了,侯捷的代碼寫得也談不上漂亮,因此我乾脆動手從新擼了一個。spa
測試程序演示了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系統特有的一種線程同步的核心對象,我的感受設計得有些複雜了。但不能否認,正是由於它的多面性,在實際開發中,它的出場概率但是至關高的。能把這個同步的核心對象用好的程序員,其它的幾個同步的核心對象就統統不在話下了。