最近工做中要维护一个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系统特有的一种线程同步的核心对象,我的感受设计得有些复杂了。但不能否认,正是由于它的多面性,在实际开发中,它的出场概率但是至关高的。能把这个同步的核心对象用好的程序员,其它的几个同步的核心对象就统统不在话下了。