• 多线程之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系统特有的一种线程同步的核心对象,个人感觉设计得有些复杂了。但不可否认,正是因为它的多面性,在实际开发中,它的出场几率可是相当高的。能把这个同步的核心对象用好的程序员,其它的几个同步的核心对象就通通不在话下了。

  • 相关阅读:
    【WCF】无废话WCF入门教程
    【IIS8】在IIS8添加WCF服务支持
    iOS 中如何将View设置为圆角的矩形?
    在iOS开发中使用FMDB
    iOS中FMDB的使用【单例】
    普通分页笔记
    基础BaseDao
    连接池技术 实现的四个要素:jdbc.properties配置- 读取配置的单例类 --ConfigManage--BaseDao写法
    反射生成对象,调用对象方法
    context分页
  • 原文地址:https://www.cnblogs.com/oowgsoo/p/9508374.html
Copyright © 2020-2023  润新知