MFC开发中的多线程封装
目录
一、 基本知识:
MFC中有两类线程,分别称之为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。
工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务。
1. 线程的启动与结束
1) 线程的启动
在MFC中启动线程可以使用如下几种方式:
l Win32SDK中标准形式的API函数::CreateThread(),这个函数直截了当地创建一个线程;
l C标准运行时库中的_beginthread或_beginthreadex,下面会讲到两者的不同;
l MFC类库CWinThread的成员函数CWinThread::CreateThread(),在该函数中实际上调用CRTL的_beginthreadex函数创建线程,并将线程句柄等封装在CWinThread类中;
l MFC的API函数AfxBeginThread(),该函数实际上是先创建了MFC中对应的CWinThread对象,调用对象的CreateThread()方法完成线程创建;
2) 线程的结束
对应于线程的启动,MFC中采用如下方法结束线程:
l 从CreateThread时指定的线程函数返回,或显式调用Win32SDK函数::EndThread都会结束线程。
l 从_beginthread或_beginthreadex调用中指定的线程启动函数返回或分别显式调用_endthread或_endthreadex都会结束线程(从线程函数返回时会隐式调用_endthread或_endthreadex,但可以显示调用它们帮助正确释放线程资源。)
l 对CWinThread::CreateThread()创建的线程应使用向对象发送WM_QUIT消息的方式结束线程;
l 对应AfxBeginThread(),MFC提供了EndThread()这个API来中止线程,但要注意AfxEndThread这个函数智能在线程函数中调用。
3) 深入剖析线程的创建与结束函数
l 使用SDK函数::CreateThread()创建函数时要考虑进程的同步与互斥关系,进程间的同步与互斥等一系列会导致操作系统死锁的因素,用起来比较繁琐一些,初学的人在用到的时候可能会产生不可预料的错误。其中要考虑的一些因素有:
(1)C Runtime中需要对多线程进行纪录和初始化,以保证C函数库工作正常(典型的例子是strtok函数,原因是这些函数中有静态变量)。
(2)MFC也需要知道新线程的创建,也需要做一些初始化工作(当然,如果没用MFC就没事了)。
l _beginthread和_beginthreadex的异同:
(1)_beginthread和_beginthreadex成功时都返回创建成功的线程句柄,但失败时_beginthread返回-1,而_beginthreadex失败时返回0;
(2)_beginthread创建线程时使用的线程入口函数必须是__cdecl标准C/C++函数调用,且无返回值,函数返回时线程自动结束。而_beginthreadex创建线程的线程入口函数使用__stdcall调用方式;
(3)_beginthread和_beginthreadex都提供了对C运行库中某些变量的初始化,使它们可以在多线程中使用;
(4)_endthread和_endthreadex在线程函数返回时都会自动调用,并结束线程,并且它们也可以在线程函数返回前显式调用来帮助释放线程资源和结束线程;
(5)_endthread会自动关闭线程句柄,而_endthreadex不会自动关闭线程句柄,因此使用_endthread关闭线程后不要使用Win32的API函数::CloseHandle()来关闭句柄。
l CWinThread()::CreateThread函数首先完成线程对象的初始化工作,然后调用_beginthreadex来创建线程。注意在CWinThread类中有一个成员变量m_bAutoDelete,为TRUE则线程结束时线程对象被自动删除,为FALSE则线程对象不被删除。
l AfxBeginThread()首先创建一个CWinThread对象,然后调用CWinThread::CreateThread创建线程。
4) 启动与结束的封装:
l 在MFC下对CWinThread类进行继承;
l 不要在继承类的构造函数中调用CWinThread::CreateThread来启动线程,将线程启动封装为一个函数,函数中使用成员变量m_bCreated记录线程是否已经启动:
void ConnectNewThread::create()
{
if (m_bCreated == FALSE)
{
this->CreateThread();//Startthe thread immediately after creation.
M_bCreated = TRUE;
}
}
l 线程的结束使用向线程发送WM_QUIT消息的方式结束线程,因此要求创建的线程对象具有消息循环。结束线程时,如果线程正在进行消息响应,则线程需要完成消息响应才能重新进入消息循环,从而响应WM_QUIT消息结束线程,因此如果希望在执行多次操作过程中途打断操作,则不能将多次操作置于一次消息响应中,如果这样的话多次操作执行完成后才会重新进入消息循环,因此无法实现中途打断。
void ConnectNewThread::quit()
{
if(m_bCreated == TRUE) /* 可以在线程执行中向自己发送线程退出消息 */
{
this->PostThreadMessage(WM_QUIT,0,0);
}
}
l 线程的退出要考虑需要等待线程结束再继续执行的问题,通常使用WaitForSingleObject()或WaitForMultiObjects()函数,但使用时应在其他线程中进行等待,如果在本线程中等待则本线程永远不会退出,但是使用这两个函数也不是安全的,例如:在主线程中等待子线程退出,而子线程由于操作MFC控件(需要向主线程SendMessage)而主线程不能响应,从而造成子线程与主线程都被死锁。为了解决这个问题,微软提供了MsgWaitForMultipleObject()函数用于等待线程退出。
void ConnectNewThread::wait()
{/* 不可以在线程执行过程中等待自己退出,会死锁*/
while(TRUE && m_bCreated == TRUE)
{
intnCount = 1; /* 等待线程的数目 */
DWORDret = ::MsgWaitForMultipleObjects
(nCount,&this->m_hThread,FALSE,INFINITE,QS_ALLINPUT);
if(ret >= WAIT_OBJECT_0 && ret <= WAIT_OBJECT_0 + nCount - 1)
{
TRACE("ConnectNewThread等待线程退出成功.\n");
/*等待线程句柄成功 */
break;
}
elseif(ret == WAIT_OBJECT_0 + nCount)
{
/*消息到达 */
MSGmsg;
TRACE("ConnectNewThread等待线程退出时收到消息.\n");
while(::PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
continue;
}
elseif (ret == WAIT_FAILED)
{
/*等待失败 */
DWORDdErrCode=GetLastError();
TRACE("ConnectNewThread等待线程结束失败.\n失败原因:%d.\n",dErrCode);
break;
}
else
{
/*意外情况 */
TRACE("等待到意外情况.\n");
continue;
}
}
m_bCreated= FALSE;
}
l 如果需要多次使用线程对象,则需要线程对象在线程结束时不会自动销毁,需要在构造函数中将m_bAutoDelete成员变量置为FALSE(默认为TRUE),并且在线程对象被真正销毁时要求线程退出,否则线程在已经销毁的对象中运行就会将程序跑飞。因此在析构函数中要保证线程正常退出。
ConnectNewThread::~ConnectNewThread()
{
this->quit();
this->wait();
}
2. 线程的执行过程
1) 线程对象的初始化
l virtual BOOL InitInstance();
(1) 该函数在执行CWinThread::CreateThread()函数后被首先调用,一般重载用来完成一些线程执行前必须的初始化工作,例如为界面线程创建界面,并将创建的界面指针设置为线程主窗口指针;
(2) 该函数如果返回TRUE,则创建的线程将具有消息循环,线程为界面线程;如果返回FALSE,则创建的线程没有消息循环。
(3) 使用AfxBeginThread()创建工作者线程时传入的线程入口函数指定给CWinThread类的成员变量m_pfnThreadProc函数指针,如果该指针不为空就认为是工作者线程,否则认为是界面线程。如果是界面线程,会在未设置主窗口时将CWinApp的主窗口设置为线程的主窗口。
l virtual int Run();
(1) 该函数为界面线程类型的CWinThread类提供了消息循环,通常不进行重载;
(2) 该函数一直接收并分发Windows消息,直到收到WM_QUIT消息时返回;
(3) 该函数在空闲(未收到消息)时调用OnIdle()函数进行空闲处理;
l virtual int ExitInstance();
(1) 该函数在Run()返回或者InitInstance()返回FALSE时得到调用,用于对InitInstance中的资源分配进行清理;
(2) CWinThread的默认处理中如果m_bAutoDelete成员变量为TRUE则删除CWinThread对象;
(3) 该函数需要调用基类版本。
2) 线程功能函数的执行
l 对于界面线程,要在线程中执行任务需要向线程对象发送消息,在消息响应中执行要执行的任务;
(1)用户自定义消息:
#define WM_BUILD_CONNECT_START WM_USER+0 /*开始建立连接操作 */
(2)添加消息映射:
BEGIN_MESSAGE_MAP(ConnectNewThread,CWinThread)
ON_THREAD_MESSAGE(WM_BUILD_CONNECT_START, OnBuildConnectStarted)
END_MESSAGE_MAP()
(3)线程消息响应函数:
void ConnectNewThread::OnBuildConnectStarted(WPARAMwParam,LPARAM lParam)
{
/* 在子线程的消息循环中响应建立连接消息 */
boolret = TcpConnector::GetObj()->DoBuildConnection();
if(ret == true)
{
TRACE("尝试连接服务器成功\n");
m_bResult= TRUE;
}
else
{
TRACE("尝试连接服务器失败\n");
m_bResult= FALSE;
}
PostStopMessage();
m_bStoped= TRUE;
}
3. 线程间的通信
1) 主线程向子线程发送消息
主线程只要通过CWinThread或其继承类的指针就可以实现向子线程发送消息,具体见第二部分;
2) 子线程向主线程发送消息
子线程向主线程发送消息具有如下几种方法:
l 向子线程通过参数传递将主线程ID传入子线程,在子线程中使用::PostThreadMessage()向主线程发送消息,这种方法可以适用于所有类型的线程间通信(获取线程ID使用GetCurretnThreadID()函数获取);
l 向子线程通过参数传递将主线程窗口的句柄传入子线程,在子线程中由窗口句柄获得窗口指针,并通过PostMessage()或SendMessage()向窗口发送消息,在窗口中进行消息响应。
3) 线程间通信的注意事项
l 不能直接在线程间传递窗口指针或其他MFC对象指针,应使用传递句柄的方法代替,例如CDialog*应传递HWND句柄代替,CSocket应传递SOCKET句柄代替;
l 在接收句柄的线程中不要通过HWND句柄或SOCKET句柄用对应类的如CDialog::FromHandle()来得到对象指针,因为Cxxxx::FromHandle()函数返回的将是一个临时对象指针,其成员变量不具有意义(除了句柄成员变量),而调用其成员方法更是没有意义且会造成ASSERT断言失败。应该只使用CWnd::FromHandle()得到一个CWnd*指针,而通过CWnd*向对应的窗口发送消息(使用PostMessage(),不要使用SendMessage()),在消息响应中操作控件。
4) 线程间通信的封装
l 对于子线程可能需要向多个窗口通知某项消息(如某项操作已经结束),因此对于子线程需要一个队列管理这些窗口句柄,而相对子线程来说这些窗口可以说是一个观察者,因此使用一个观察者队列。为了代码的可重用性考虑,可以参考CObject类中DELCARE_DYNCREATE和IMPLAMENT_DYNCREATE宏定义的方法将这些队列操作与消息发送封装到宏定义中(也可以使用继承的方法进行封装)。
l 观察者队列类型定义:
#include <vector>
typedef std::vector<HWND> Observer_Array;
typedef std::vector<HWND>::iterator Observer_Itor;
typedef std::vector<HWND>::const_iterator Observer_ConstItor;
l 观察者声明:
#define DECLARE_OBSERVER_ARRAY()\
Observer_Array m_observerArray; /* 需要通知的“监视者”队列 */\
voidaddObserver(CWnd* pWnd); /* 在主线程中调用,记录作为监视者的窗口句柄 */\
voidsubObserver(CWnd* pWnd); /* 在主线程中调用,取消作为监视者的窗口句柄 */\
voidpostObserverMessage(UINT message) /* 在子线程中调用,向监视者队列发送消息*/;
l 观察者实现:
#define IMPLEMENT_OBSERVER_ARRAY(classname)\
voidclassname::addObserver(CWnd *pWnd)\
{ \
if (pWnd == 0|| !pWnd->IsKindOf(RUNTIME_CLASS(CWnd)))\
{\
TRACE("错误的“监视者”\n");\
return;\
}\
Observer_ConstItoritor = m_observerArray.begin();\
HWND hWnd =pWnd->GetSafeHwnd();\
while (itor!= m_observerArray.end())\
{\
if(hWnd == *itor)\
{\
return;\
}\
itor++;\
}\
m_observerArray.push_back(hWnd);\
}\
voidclassname::subObserver(CWnd *pWnd)\
{\
if (pWnd == 0|| !pWnd->IsKindOf(RUNTIME_CLASS(CWnd)))\
{\
TRACE("错误的“监视者”\n");\
return;\
}\
Observer_ConstItoritor = m_observerArray.begin();\
HWND hWnd =pWnd->GetSafeHwnd();\
while (itor!= m_observerArray.end())\
{\
if(hWnd == *itor)\
{\
m_observerArray.erase(itor);\
break;\
}\
itor++;\
}\
}\
voidclassname::postObserverMessage(UINT message)\
{\
Observer_ConstItoritor = m_observerArray.begin();\
while (itor!= m_observerArray.end())\
{\
HWNDhWnd = (*itor);\
CWnd*pWnd = CWnd::FromHandle(hWnd);\
pWnd->PostMessage(message);\
itor++;\
}\
}