多線程之CEvent

最近工做中要維護一個windows模塊,用到了mfc中的CEvent類。這算是好久好久之前的老朋友了吧,估計和我超過10年沒見過面了,不過工做就是工做,技術上來不得半點含糊,因此仍是從新認識一下這位老朋友吧。程序員

https://img2.mukewang.com/5b7aaf5b0001ff0b11840936.jpg

本文用一個具體的例子來對CEvent類進行介紹,基本上掌握了這個例子後,咱們就算是完全認識CEvent類了。其實其它windows多線程同步的內核對象也大致如此,這是一幫老朋友們。windows

1.CEvent類多線程

CEvent的接口不多:函數

https://img3.mukewang.com/5b7ab10d000152cf08460182.jpg

基類就更簡單了:工具

https://img1.mukewang.com/5b7ab1400001c8f107710227.jpg

其實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

https://img1.mukewang.com/5b7ab23a000171fa05760389.jpg

若是有足夠老的程序員應該對這個界面不會陌生,它和侯捷的那本《win32多線程程序設計》中的CEvent例子很象。這本書實在是太老了,侯捷的代碼寫得也談不上漂亮,因此我乾脆動手從新擼了一個。spa

測試程序演示了CEvent的兩種模式:自動模式和手動模式,並分別對幾個類方法進行了測試。線程

這是一個標準的MFC對話框程序,開發工具用VS2017:

https://img1.mukewang.com/5b7ab4380001e0b009550660.jpg

怎麼經過嚮導創建工程?怎麼在資源裏拖放控件?怎麼創建消息映射等等太簡單了,我就跳過了,下面將主要講解主窗口的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();
}

https://img2.mukewang.com/5b7abe110001c1da05760389.jpg

沒錯,一次就放進來一我的吃飯。由於是自動模式,開門後剛進來一我的,門就自動關上了。

點擊三次後:

https://img1.mukewang.com/5b7abe7100014f4a05760389.jpg

點了三次才進來三我的,就是這麼費勁。因此,你大可把自動模式想象成曾今的國營飯店,一次只能服務一桌客人。

關門,點擊ResetEvent:

void CEventDemoDlg::OnBnClickedButtonResetEvent()
{
    // TODO: 在此添加控件通知處理程序代碼
    m_event->ResetEvent();
}

沒有任何反應。由於自動模式是自帶關門功能的。

點擊PulseEvent:

void CEventDemoDlg::OnBnClickedButtonPulseEvent()
{
    // TODO: 在此添加控件通知處理程序代碼
    m_event->PulseEvent();
}

pulse是脈衝的意思,這表明一次放進去一波客人,不過在自動模式下,由於門關得太快,一次也只能一個客人。因此這個效果和點擊SetEvent是同樣的。

點擊PulseEvent三次後:

https://img2.mukewang.com/5b7abfb000015e0205760389.jpg

5.手動模式

在界面上切換到手動模式:

void CEventDemoDlg::OnBnClickedRadioManual()
{
    // TODO: 在此添加控件通知處理程序代碼
    ExitThread();

    ExitEvent();
    InitEvent(TRUE);

    InitThread();
}

首先銷燬了工做線程,銷燬了核心對象。而後重建新的核心對象和工做線程。

這裏的TRUE表示是手動模式。此時門是關着的。

初始化工做線程。這些傢伙都在門口等着吃飯。

下面點擊SetEvent開門:

void CEventDemoDlg::OnBnClickedButtonSetEvent()
{
    // TODO: 在此添加控件通知處理程序代碼
    m_event->SetEvent();
}

https://img2.mukewang.com/5b7ac3c70001c61105760389.jpg

大門一開,工做線程們果真如脫繮的野馬般跑個不停。

趕忙點擊關門:

void CEventDemoDlg::OnBnClickedButtonResetEvent()
{
    // TODO: 在此添加控件通知處理程序代碼
    m_event->ResetEvent();
}

工做線程們終於停下來了。

點擊Clear Result清理一下狼藉的現場。

void CEventDemoDlg::OnBnClickedButtonClearResult()
{
    // TODO: 在此添加控件通知處理程序代碼
    m_result.ResetContent();
}

https://img3.mukewang.com/5b7ac463000125ed05760389.jpg

此次試一試點擊PulseEvent:

void CEventDemoDlg::OnBnClickedButtonPulseEvent()
{
    // TODO: 在此添加控件通知處理程序代碼
    m_event->PulseEvent();
}

https://img.mukewang.com/5b7ac4ae00016ef205760389.jpg

這就是脈衝的意思,一次將門口正在等待的一波工做線程通通放進來,而後關門。

5.後記

CEvent是Windows系統特有的一種線程同步的核心對象,我的感受設計得有些複雜了。但不能否認,正是由於它的多面性,在實際開發中,它的出場概率但是至關高的。能把這個同步的核心對象用好的程序員,其它的幾個同步的核心對象就統統不在話下了。

相關文章
相關標籤/搜索