多線程之CEvent


最近工作中要維護一個windows模塊,用到了mfc中的CEvent類。這算是很久很久以前的老朋友了吧,估計和我超過10年沒見過面了,不過工作就是工作,技術上來不得半點含糊,所以還是重新認識一下這位老朋友吧。

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

本文用一個具體的例子來對CEvent類進行介紹,基本上掌握了這個例子后,我們就算是徹底認識CEvent類了。其實其它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,測試用例當然是帶界面的了:

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

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

測試程序演示了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系統特有的一種線程同步的核心對象,個人感覺設計得有些復雜了。但不可否認,正是因為它的多面性,在實際開發中,它的出場幾率可是相當高的。能把這個同步的核心對象用好的程序員,其它的幾個同步的核心對象就通通不在話下了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM