首先声明, 这里的工作线程与UI线程是相对的,即没有任何窗口的. 如果需要与主线程或其它辅助线程通讯,有几种方法如事件,消息,信号等,也可以是以上几种方法的综合运用.下面就列出以下3种通讯方法的代码框架
(1)只用消息通讯
2 {
3 //创建线程消息队列
4 MSG msg;
5 PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
6 //通知其它线程消息队列已创建好
7 SetEvent(hEvent);
8
9 while(true)
10 {
11 GetMessage(&msg, NULL, 0, 0);
12 switch(msg.message)
13 {
14 case WM_QUIT:
15 return 1;
16
17 //自定义消息1处理
18 case WM_USER + 100:
19 break;
20
21 //自定义消息2处理
22 case WM_USER + 101:
23 break;
24 }
25 }
26 return 0;
27 }
(2)只用事件通讯
2 {
3 DWORD dwIndex;
4 while (true)
5 {
6 dwIndex = WaitForMultipleObjects(cObjects, pObjects, FALSE, INFINTE);
7 if (WAIT_OBJECT + 0== dwIndex)
8 {
9 return 1; //假设为退出事件
10 }
11 else if (WAIT_OBJECT + 1 == dwIndex)
12 {
13 //事件1受信,处理之
14 }
15
16 else if (WAIT_OBJECT + cObjects - 1 == dwIndwx)
17 {
18 //事件cObjects - 1受信, 处理之
19 }
20 }
21 }
(3)用消息和事件通讯
2 {
3 while (TRUE)
4 {
5 DWORD ret ;
6 MSG msg ;
7
8 while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
9 {
10 switch(msg.message)
11 {
12 //线程退出消息,直接返回
13 case WM_QUIT:
14 return 1;
15
16 //自定义消息1处理
17 case WM_USER + 100:
18 break;
19 //自定义消息2处理
20 case WM_USER + 101:
21 break;
22 }
23 }
24 ret = MsgWaitForMultipleObjects(cObjects, lphObjects, FALSE,INFINITE,QS_POSTMESSAGE);
25 if (ret == (WAIT_OBJECT_0 + cObjects))
26 {
27 //有新的消息到来,继续到上步PeekMessage处理
28 continue;
29 }
30 else
31 {
32 //是事件受信了
33 if (ret == WAIT_OBJECT_O)
34 {
35 }
36 else if (ret == WAIT_OBJECT_O + 1)
37 {
38 }
39 else if(ret == WAIT_OBJECT_O + cObjects - 1)
40 {
41 }
42 }
43 return 0;
44 }
上面用到了GetMessage和PeekMessage 函数, 这两者都是从消息队列取出消息, 不同的是GetMessage从消息队列删除消息,并且阻塞调用线程. PeekMessage则是查询消息队列,如果有消息就取出,没有消息也立即返回, 是否从消息队列删除消息由最后一个参数决定:PM_REMOVE表示删除,PM_NOREMOVE表示不删除.可以简单地认为,GetMessage是同步的,PeekMessage是异步的。
在MFC程序中创建一个线程,宜调用AfxBeginThread函数。该函数因参数不同而具有两种重载版本,分别对应工作者线程和用户接口(UI)线程,关于UI线程和工作者线程的分配,最好的做法是:将所有与UI相关的操作放入主线程,其它的纯粹的运算工作交给独立的数个工作者线程。
用户接口(UI)线程:主要是用于处理用户的输入并响应各种事件和消息,他是由CWinThread派生出来的
工作者线程:进行程序的后台任务,如计算调度等,它不需要重CWinThread类派生出来进行创建,它其实就是一个函数,这个函数完成该线程并行的工作,由其它语句调用这个线程启动。
工作者线程
CWinThread *AfxBeginThread(
AFX_THREADPROC pfnThreadProc, //控制函数
LPVOID pParam, //传递给控制函数的参数
int nPriority = THREAD_PRIORITY_NORMAL, //线程的优先级
UINT nStackSize = 0, //线程的堆栈大小
DWORD dwCreateFlags = 0, //线程的创建标志
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //线程的安全属性
);
工作者线程编程较为简单,只需编写线程控制函数和启动线程即可。下面的代码给出了定义一个控制函数和启动它的过程:
//线程控制函数
UINT MfcThreadProc(LPVOID lpParam)
{
CExampleClass *lpObject = (CExampleClass*)lpParam;
if (lpObject == NULL || !lpObject->IsKindof(RUNTIME_CLASS(CExampleClass)))
return - 1; //输入参数非法
//线程成功启动
while (1)
{
...//
}
return 0;
}
eg:
建立基于对话框的工程,建立控制函数:ThreadProc.h和ThreadProc.cpp以及建立一个基于CWinThread的类CMyUI;
Cmytest17Dlg.CPP:
void Cmytest17Dlg::OnBnClickedButton1()
{
}
ThreadProc.h
UINT ThreadPro(LPVOID wParam);
ThreadProc.cpp
#include "stdafx.h"
#include "ThreadProc.h"
#include "mytest17Dlg.h"
#include "MyUI.h"
UINT ThreadPro(LPVOID wParam)
{
}
MyUI.h
pragma once
#define WM_MYMSG_ONE
#define WM_MYMSG_TWO
// CMyUI
class CMyUI : public CWinThread
{
protected:
public:
protected:
};
MyUI.CPP
#include "stdafx.h"
#include "mytest17.h"
#include "MyUI.h"
// CMyUI
void CMyUI::MsgOneProc(UINT mParam, LONG lParam)
{
}
void CMyUI::MsgTwoProc(UINT mParam, LONG lParam)
{
}
IMPLEMENT_DYNCREATE(CMyUI, CWinThread)
CMyUI::CMyUI()
{
}
CMyUI::~CMyUI()
{
}
BOOL CMyUI::InitInstance()
{
}
int CMyUI::ExitInstance()
{
}
BEGIN_MESSAGE_MAP(CMyUI, CWinThread)
调用过程其实并不复杂,就是他提供一个Dll,并输出一个函数(姑且叫做Foo()吧),我调用他的函数Foo,其内部产生一个窗口。但是,我每次调用,窗口总是一闪而过!我们于是怀疑,是不是因为主程序是console,不支持MFC的缘故?要不然一定是因为不是在主线程里调用的缘故?!又是多线程,真是麻烦!
反复测试,结果如下
MFC对话框Win32 MFC Console
多线程 失败 失败
单线程 成功 失败
为什么?难道就不能在普通线程里创建窗口吗?什么是主线程(UI线程),什么又是工作线程?以前认为理所当然的事情,现在突然变得很模糊!
开始把目光锁定到消息队列上!是不是因为主线程有消息队列?比如MFC对话框程序就有消息队列,而Console没有。但是,同样是线程,为什么你有而我没有?难道是WinMain函数和普通的main函数不一样?到底消息队列是什么时候产生的?
草草浏览了CWinApp的代码,也没有找到答案。这时候,隐约想起了《Windows核心编程》里说的,系统在你第一次调用相关函数的时候,自动为线程创建消息队列!但是,到底是什么函数呢?
带着这个郁闷回到了家,打开那本Windows编程宝典,翻到消息队列一章。原来,每个窗口归属于创建它的线程。一旦线程退出,窗口也将自行销毁!另外,线程还要进行消息循环,以分发窗口的消息!注意,消息循环是由线程负责的!线程刚开始创建的时候是不带消息队列的,当线程第一次调用窗口相关的函数时,操作系统自动为其创建消息队列!原来,窗口的消息队列是归属于线程的!
问题终于搞清楚了,在普通的工作线程里,我们没有消息循环,所以创建出来的窗口还没有没有机会显示出来,线程就结束退出了!即使在console程序里,通过cin.get()让程序等待,窗口还是没有机会刷新(依赖消息循环分发并调用消息处理函数)而一直处于忙碌等待状态!而前面的Foo函数,恰恰要依赖于消息循环!
知道问题的真正原因后,就很容易解决了!在调用同事的函数产生窗口后(函数返回了),在同一个线程里进行消息循环!如下
...in a working thread...
Foo(); //call some function
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
...working thread exit...
问题迎刃而解!你看不是MFC的问题,也不是不能在工作线程里创建窗口。本来,我一开始就怀疑,这么依赖MFC,总不能让主程序都是MFC程序吧!作为一个DLL组件,不能对调用者有太多的限制!而多线程,太寻常了,如果都在主线程里做,那也太难模块化了吧!当然了,现在说这些都是马后炮,当时郁闷的时候,什么古怪的理由都用上了!
其实,这个问题本来可以解决得更快点的。如果同事事先说明他的Foo函数何时返回(我一直以为它要一直阻塞到其产生的窗口关闭才返回的,结果并不是这样的,窗口一产生它就返回了),以及是否需要依赖外部的消息循环(他是有提过,但也很模糊,搞不清具体为什么)。当然,最大的原因还是因为大家对Windows的消息循环机制理解得不是很深入!
现在你搞明白Windows的消息循环到底是如何进行的了么?
以前写MFC的DLL的时候,总会在自动生成的代码框架里看到提示,需要在每一个输出的函数开始添加上AFX_MANAGE_STATE(AfxGetStaticModuleState())。一直不明白这样做的含义,也一直没有这样做,而且代码也工作得好好的,所以感觉这好像一句废话。
最近的项目中,需要在DLL里使用MFC生成界面,这才发现一旦资源放在不同的动态库里,而且还和多线程搅和在一起的时候,事情就变得异常的复杂,以前对MFC的一知半解已经不足与应付了。程序莫名的崩溃,莫名的ASSERT,资源怎样也装载不起来,为什么呢?每次,总是尝试着,在每一个线程的开始,把AFX_MANAGE_STATE(AfxGetStaticModuleState())添加上去,或者在某些地方用AfxSetResourceHandler()一把,然后问题就解决了,但是不是很明白到底是怎么回事,总感觉这种解决办法让人很不安心,仿佛在下一秒问题又会突然冒出来。
前天,这个问题终于发挥到了极致,任我花费了好几个小时,怎样的尝试都不能成功,在项目的关键时候发生这种事情,让我暗暗发誓以后再也不用MFC了。正像很多的电影情节一样,事情最后还是得到了解决,这次我决定不能再这么算了,一定要把这个事情理解得明明白白。
在这里,我遇到的问题就是,如何让DLL里的界面代码使用该DLL的资源(Resource),如何在工作线程里加载有IE控件的对话框?
我问同事,他们是如何实现DLL资源切换的?AFX_MANAGE_STATE(AfxGetStaticModuleState())这就是他们的答案,一如微软的推荐,原来就是这么简单啊!让我们来看看,这句代码到底做了什么?
#define AFX_MANAGE_STATE(p) AFX_MAINTAIN_STATE2 _ctlState(p);
AFX_MAINTAIN_STATE2::AFX_MAINTAIN_STATE2(AFX_MODULE_STATE* pNewState)
{
m_pThreadState = _afxThreadState;
m_pPrevModuleState = m_pThreadState->m_pModuleState;
m_pThreadState->m_pModuleState = pNewState;
}
_AFXWIN_INLINE AFX_MAINTAIN_STATE2::~AFX_MAINTAIN_STATE2()
{ m_pThreadState->m_pModuleState = m_pPrevModuleState; }
原来,就是定义一个局部的对象,利用其构造和析构函数在函数的入口和函数的出口进行State状态的切换,我猜AfxGetStaticModuleState()一定是获取当前代码所在DLL的State。
果然,请看
static _AFX_DLL_MODULE_STATE afxModuleState;
AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState()
{
AFX_MODULE_STATE* pModuleState = &afxModuleState;
return pModuleState;
}
class _AFX_DLL_MODULE_STATE : public AFX_MODULE_STATE
// AFX_MODULE_STATE (global data for a module)
class AFX_MODULE_STATE : public CNoTrackObject
{
...
CWinApp* m_pCurrentWinApp;
HINSTANCE m_hCurrentInstanceHandle;
HINSTANCE m_hCurrentResourceHandle;
LPCTSTR m_lpszCurrentAppName;
BYTE m_bDLL; // TRUE if module is a DLL, FALSE if it is an EXE
...
COccManager* m_pOccManager;
...
这里不得不说,MFC把很多的数据都堆放在这里,搞得很复杂,结构性非常的差。
}
afxModuleState是dll的静态成员,自然可以被同样的dll里的代码所访问,但是何时初始化的?
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
...
AfxWinInit(hInstance, NULL, _T(""), 0);
...
}
BOOL AFXAPI AfxWinInit(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
// handle critical errors and avoid Windows message boxes
SetErrorMode(SetErrorMode(0) |
SEM_FAILCRITICALERRORS|SEM_NOOPENFILEERRORBOX);
// set resource handles
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
pModuleState->m_hCurrentInstanceHandle = hInstance;
pModuleState->m_hCurrentResourceHandle = hInstance;
...
}
原来在DLL的入口函数,用该DLL的hInstance初始化了该结构。
到这时候,我们还是不明白,为什么要进行资源切换?前面开始的_afxThreadState到底是什么?好像跟Thread有关系,到底是什么呢?
THREAD_LOCAL(_AFX_THREAD_STATE, _afxThreadState)
#define THREAD_LOCAL(class_name, ident_name) /
AFX_DATADEF CThreadLocal<class_name> ident_name;
template<class TYPE>
class CThreadLocal : public CThreadLocalObject
再往下跟踪,发现其实代码越发生涩难懂,但是基本的功能就是访问当前此行代码的线程的私有数据。所谓线程的私有数据,就是说,不同的线程执行同样的一段代码,得到的数据可能是不同的。这才想起来,MFC的很多句柄啦,都是保存在全局的Map里的,而且放在线程的私有数据区里,所以跨线程传递MFC对象是很不安全的。但是,MFC为什么要这么做呢?这个问题,到目前为止,我还是搞不明白。
还是回到开始的代码,资源切换到底是如何进行的?
int CDialog::DoModal()
{
...
HINSTANCE hInst = AfxGetResourceHandle();
if (m_lpszTemplateName != NULL)
{
hInst = AfxFindResourceHandle(m_lpszTemplateName, RT_DIALOG);
HRSRC hResource = ::FindResource(hInst, m_lpszTemplateName, RT_DIALOG);
hDialogTemplate = LoadResource(hInst, hResource);
...
}
_AFXWIN_INLINE HINSTANCE AFXAPI AfxGetResourceHandle()
{ ASSERT(afxCurrentResourceHandle != NULL);
return afxCurrentResourceHandle; }
#define afxCurrentResourceHandle AfxGetModuleState()->m_hCurrentResourceHandle
AFX_MODULE_STATE* AFXAPI AfxGetModuleState()
{
_AFX_THREAD_STATE* pState = _afxThreadState;
AFX_MODULE_STATE* pResult;
if (pState->m_pModuleState != NULL)
{
// thread state's module state serves as override
pResult = pState->m_pModuleState;
}
else
{
// otherwise, use global app state
pResult = _afxBaseModuleState.GetData();
}
ASSERT(pResult != NULL);
return pResult;
}
原来MFC的对话框装载资源是通过获取当前线程对应的ModuleState保存的ResourceHandler来装载资源的。所以,DLL里的代码,需要在函数的入口,首先把当前执行线程的ModuleState换成该Dll的State,这样才能装载该dll的资源!这时候,我突然明白过来,为什么需要要依赖线程的私有数据来保存ModuleState,其实确切的说是传递!--这其实是因为CDialog是存放在另一个DLL里的,比如MFC40.dll,如果以共享模式连接MFC库的话。而用户自己编写的CDialog的子类并不放在CDialog同样的Dll里,他们如何来传递这个资源句柄呢?两种解决办法:1,利用参数传递。2,存放在一个公共的地方。前者需要增加参数,显得很麻烦,Win32的API好像就是这样实现的吧?后者,需要确定这个公共地方在何处?这让人想起来,建立一个公共的动态库?由主程序的提供?再多说一句,J2EE里有一个容器的概念(COM+好像也有,不知道.NET是如何的),组件都是生存在容器里,这时候我们就可以设想把该数据存放在容器里。不管怎样,MFC的实现就是放在线程的私有数据区,不需要公共的动态库,也不需要麻烦主程序,它自己就搞定了!它自以为很好的解决方式,很完美,却引发了我们的一系列的问题,特别是不明白就里的人。
关于资源装载,问题似乎已经解决了,但是还有一点点小麻烦就是,我实现的dll不是以普通的输出函数进行输出的,而是输出类,我可不想在每一个类的成员函数里添加AFX_MANAGE_STATE(AfxGetStaticModuleState())。怎么办呢?既然已经知道了资源切换的原理,我们添加两个输出函数,分别对应AFX_MAINTAIN_STATE2的构造和析构函数,在类的使用前后调用,就可以了。或者,分别放在类的构造和析构函数里。又或者,就声明为成员变量。无论怎样,需要保证的一点就是资源的切换要正确嵌套,不可交叉--这种情况在不同的DLL之间交叉调用的时候会发生。
好了,现在DLL里的资源可以正确调用了,但是在当Dialog上包含有IE控件的时候,我们还是失败了,为什么呢?我知道对于ActiveX控件,Dialog需要做一些特殊的处理,AfxEnableControlContainer(),我也知道,要使用COM,需要CoInitialize(),但是我一直没有想过需要两个一起用才能把IE弄出来,但是最后就是这样的。奇怪的是,如果不是在工作线程里,根本不需要CoInitialize(),就能装载IE控件的,这个暂时就先不管了。
PROCESS_LOCAL(COccManager, _afxOccManager)
void AFX_CDECL AfxEnableControlContainer(COccManager* pOccManager)
{
if (pOccManager == NULL)
afxOccManager = _afxOccManager.GetData();
else
afxOccManager = pOccManager;
}
#define afxOccManager AfxGetModuleState()->m_pOccManager
这样看来,这个_afxOccManager应该是属于整个进程的,整个进程只有一个,就在那个定义它的dll里。但是,你需要把该对象(或者创建一个自定义的)传给ModuleState(请注意前面的AFX_MODULE_STATE里就包含了该属性),也就是要AfxEnableControlContainer()一下,这样特定的ModuleState就有了OccManager的信息!但是,请注意,一定要在目标dll里,正确切换了资源之后,才能进行,如下:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CoInitialize(NULL);
AfxEnableControlContainer();
至此,这个困扰我很久的问题,终于脉络清晰起来了。
DWORD CTestMFCDlg::ThreadFunc(PVOID yy)
{
CAboutDlg dlg;
dlg.DoModal();
return 0;
}
void CTestMFCDlg::OnOK()
{
::CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)ThreadFunc,NULL,NULL,NULL);
}
在VC++6.0上编译运行出现以下ASSERT。
void CWnd::AssertValid() const
{
……
CHandleMap* pMap = afxMapHWND();
……
CObject* p;
ASSERT((p = pMap->LookupPermanent(m_hWnd)) != NULL ||
(p = pMap->LookupTemporary(m_hWnd)) != NULL);
ASSERT((CWnd*)p == this); // must be us
MFC有一个全局的Hash表(通过afxMapHWND()获得),用于把HWND句柄与MFC的封装对象CWnd进行关联,这样就可以通过CWnd::FromHandle()等函数把CWnd对象Attach到一个已有的HWND句柄上,利用MFC的封装函数可以简化对HWND的直接操作。很显然,这里的Assert是因为CWnd对象根据自身的窗口句柄(m_hWnd)从Hash表里找到CWnd对象指针与对象的本身(this)并不相同!这说明,CWnd对象创建时注册到的Hash表与目前检索的Hash表并不是同一个。为什么会是这样的呢?
CHandleMap* PASCAL afxMapHWND(BOOL bCreate)
{
AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
if (pState->m_pmapHWND == NULL && bCreate)
{
…….
pState->m_pmapHWND = new CHandleMap(RUNTIME_CLASS(CTempWnd),offsetof(CWnd, m_hWnd));
……
}
return pState->m_pmapHWND;
看来这个Hash表跟AfxGetModuleThreadState()有关,继续
AFX_MODULE_THREAD_STATE* AFXAPI AfxGetModuleThreadState()
{
return AfxGetModuleState()->m_thread.GetData();
}
AFX_MODULE_STATE* AFXAPI AfxGetModuleState()
{
_AFX_THREAD_STATE* pState = _afxThreadState;
AFX_MODULE_STATE* pResult;
if (pState->m_pModuleState != NULL)
{
// thread state's module state serves as override
pResult = pState->m_pModuleState;
}
else
{
// otherwise, use global app state
pResult = _afxBaseModuleState.GetData();
}
ASSERT(pResult != NULL);
return pResult;
}
那么_afxThreadState是什么呢?
EXTERN_THREAD_LOCAL(_AFX_THREAD_STATE, _afxThreadState)
class _AFX_THREAD_STATE : public CNoTrackObject
{
public:
_AFX_THREAD_STATE();
virtual ~_AFX_THREAD_STATE();
// override for m_pModuleState in _AFX_APP_STATE
AFX_MODULE_STATE* m_pModuleState;
AFX_MODULE_STATE* m_pPrevModuleState;
#define THREAD_LOCAL(class_name, ident_name) /
AFX_DATADEF CThreadLocal<class_name> ident_name;
#define EXTERN_THREAD_LOCAL(class_name, ident_name) /
extern AFX_DATA THREAD_LOCAL(class_name, ident_name)
分析的结果是 extern CThreadLocal<_AFX_THREAD_STATE> _afxThreadState。
template<class TYPE>
class CThreadLocal : public CThreadLocalObject
{
AFX_INLINE TYPE* GetData()
{
TYPE* pData = (TYPE*)CThreadLocalObject::GetData(&CreateObject);
ASSERT(pData != NULL);
return pData;
}
AFX_INLINE operator TYPE*()
{ return GetData(); }
AFX_INLINE TYPE* operator->()
{ return GetData(); }
static CNoTrackObject* AFXAPI CreateObject()
{ return new TYPE; }
可以看出来了,_afxThreadState是一个全局的对象。通过该对象可以获得_AFX_THREAD_STATE对象,后者是线程相关的。CThreadLocalObject的代码不再分析,大概就是检查当前的线程私有数据,如果有则返回,否则创建新的对象(即_AFX_THREAD_STATE)。
继续看AfxGetModuleState(),大致的意思是获取与当前线程相关联的AFX_MODULE_STATE对象,如果没有则获取该进程的缺省AFX_MODULE_STATE对象。
PROCESS_LOCAL(_AFX_BASE_MODULE_STATE, _afxBaseModuleState)
class _AFX_BASE_MODULE_STATE : public AFX_MODULE_STATE
#define PROCESS_LOCAL(class_name, ident_name) /
AFX_DATADEF CProcessLocal<class_name> ident_name;
#define EXTERN_PROCESS_LOCAL(class_name, ident_name) /
extern AFX_DATA PROCESS_LOCAL(class_name, ident_name)
继续看AfxGetModuleThreadState(),在获得了AFX_MODULE_STATE对象之后,访问其m_thread成员。这又是一个线程相关的数据,可以获得AFX_MODULE_STATE在不同线程中的私有数据。
// AFX_MODULE_STATE (global data for a module)
class AFX_MODULE_STATE : public CNoTrackObject
{
// define thread local portions of module state
THREAD_LOCAL(AFX_MODULE_THREAD_STATE, m_thread)
现在简单总结一下,AfxGetModuleState()可以获得与执行线程关联的AFX_MODULE_STATE,而AfxGetModuleThreadState()可以获得与执行线程关联的AFX_MODULE_STATE与当前执行线程关联的AFX_MODULE_THREAD_STATE。看起来有点绕,再啰嗦几句。我们可以这样理解,每一个线程在执行的时候,需要以一个Module作为缺省上下文,比如查找资源的时候,默认从该Module里查找。这就是为什么我们需要在DLL的输出函数入口处进行资源切换,其实是把调用者线程的上下文设为当前代码所在的Module,从而保证资源的正确装载。另外,Module里代码需要根据不同的执行线程保存不同的全局数据(是不是为了避免访问的冲突?),于是Module还可以有自己独有的线程相关的数据。所以,AFX_MODULE_STATE可以在不同线程中使用,而AFX_MODULE_THREAD_STATE只能在特定线程中使用。
现在再来看afxMapHWND(),其返回的与工作线程关联的Module在工作线程下的Hash表。我们现在可以想象,如果工作线程关联Module的不同,或者相同关联Module下,而不是工作线程下,Hash表是完全不同的。
现在继续检查堆栈,发现是Assert来自CWnd的消息循环
int CWnd::RunModalLoop(DWORD dwFlags)
{
……
if (!AfxGetThread()->PumpMessage())
CWinThread* AFXAPI AfxGetThread()
{
// check for current thread in module thread state
AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
CWinThread* pThread = pState->m_pCurrentWinThread;
// if no CWinThread for the module, then use the global app
if (pThread == NULL)
pThread = AfxGetApp();
return pThread;
_AFXWIN_INLINE CWinApp* AFXAPI AfxGetApp()
{ return afxCurrentWinApp; }
#define afxCurrentWinApp
AfxGetModuleState()->m_pCurrentWinApp
原来是获得当前执行线程相关的Module,并获得其包含/对应的CWinThread对象,并执行其消息循环函数。现在终于明白,为什么每个支持MFC的模块都要有一个CWinApp(从CWinThread派生)的全局对象了,原来是用于消息循环的!
请注意,如果当前线程没有相关联的Module,则返回当前进程的缺省AFX_MODULE_STATE对象,具体请参见AfxGetModuleState()。
好了,让我们来看一下,在消息循环里发生了什么。
BOOL CWinThread::PumpMessage()
{
……
if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
{
::TranslateMessage(&m_msgCur);
::DispatchMessage(&m_msgCur);
}
return TRUE;
BOOL CWinThread::PreTranslateMessage(MSG* pMsg)
{
……
// walk from target to main window
CWnd* pMainWnd = AfxGetMainWnd();
if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg))
return TRUE;
// in case of modeless dialogs, last chance route through main
// window's accelerator table
if (pMainWnd != NULL)
{
CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd);
if (pWnd->GetTopLevelParent() != pMainWnd)
return pMainWnd->PreTranslateMessage(pMsg);
原来产生Assert的CWnd对象是主窗口对象,也就是CTestMFCDlg,而不是我们后面在工作线程里创建的CAboutDlg。这个问题让我们很疑惑,为什么在CAboutDlg的消息循环里会调用CTestMFCDlg对象呢?仔细看注释,原来消息循环里试图去查找所谓的主窗口(AfxGetMainWnd),并在找到后调用它的消息预处理函数(PreTranslateMessage)。
我们看看主窗口在哪里?
_AFXWIN_INLINE CWnd* AFXAPI AfxGetMainWnd()
{ CWinThread* pThread = AfxGetThread();
return pThread != NULL ? pThread->GetMainWnd() : NULL; }
CWnd* CWinThread::GetMainWnd()
{
if (m_pActiveWnd != NULL)
return m_pActiveWnd;
// probably in-place active
// when not inplace active, just return main window
if (m_pMainWnd != NULL)
return m_pMainWnd;
return CWnd::GetActiveWindow();
}
很显然是想获取当前线程关联的Module对应的CWinThread对应的主窗口/激活窗口。
因为主线程和工作线程都没有做Module的切换工作,_afxThreadState->m_pModuleState都为空,所以两个线程执行AfxGetModuleState()的结果总是返回进程缺省的AFX_MODULE_STATE对象,即_afxBaseModuleState。所以AfxGetThread()函数返回的必然是theApp对象(CTestMFCApp),后者的m_pMainWnd成员指向的就是CTestMFCDlg对象。
在调用CTestMFCDlg的PreTranslateMessage函数的时候,其内部进行了如下检查
CFrameWnd* CWnd::GetTopLevelFrame() const
{
if (GetSafeHwnd() == NULL) // no Window attached
return NULL;
ASSERT_VALID(this);
然后进入了前面提到的CWnd::AssertValid()函数,终于导致Assert的发生。为什么呢?因为afxMapHWND()是在工作线程下运行的,而m_pMainWnd对应的CTestMFCDlg是在主线程下创建的,两者使用的Hash表是不同的。因此工作线程根据HWND查找CWnd对象时自然是找不到,于是自动创建一个新的CWnd,当然与m_pMainWnd不同啦,虽然HWND是相同的。
现在我们来想想,为什么MFC要做前面的ASSERT检查呢?为什么要求MFC对象只能在创建线程里使用而不允许跨线程使用呢?就像在ASSERT的位置的注释所言:
// Note: if either of the above asserts fire and you are
// writing a multithreaded application, it is likely that
// you have passed a C++ object from one thread to another
// and have used that object in a way that was not intended.
// (only simple inline wrapper functions should be used)
//
// In general, CWnd objects should be passed by HWND from
// one thread to another. The receiving thread can wrap
// the HWND with a CWnd object by using CWnd::FromHandle.
//
// It is dangerous to pass C++ objects from one thread to
// another, unless the objects are designed to be used in
// such a manner.
是不是为了避免资源访问冲突?比如同时操作一个对象等等,单线程总是比多线程简单很多。又或者MFC本身就使用了太多线程相关的数据,跨线程操作会导致混乱?到目前为止也还没有想清楚……
到目前为止,我们已经大概知道问题是如何出现的了,那么如何解决呢?这或许才是大多数人想要了解的。
1.使用VS2005编译运行,一切正常。
2.使用AfxBeginThread()创建线程,而不是CreateThread()。
void CTestMFCDlg::OnOK()
{
AfxBeginThread((AFX_THREADPROC)ThreadFunc,NULL);
}
因为AfxBeginThread内部会自动做一些处理,所以也没有问题。
3.另外我们还发现,如果只有一个窗口,即没有CTestMFCDlg窗口存在的情况下,CAboutDlg是可以正常显示的,因为不涉及跨线程访问MFC对象的情况。这就是为什么我们经常在非MFC的COM组件里可以多线程使用单个MFC窗口的原因。MFC并不是只能在主线程里使用,只是不能跨线程传递对象而以。
4.有没有可能使用CreateThread()创建线程,而还能在线程里显示窗口的?特别是有些代码是第三方开发的,你无法修改的时候。当然了,AfxBeginThread内部可能也是调用CreateThread这个Windows的API,既然它能,我们肯定也能,只是代码的多少而已。有没有可能像前面的文章对DLL输出函数的资源切换那么简单,就用AFX_MANAGE_STATE(AfxGetStaticModuleState())?
我们试着加上AFX_MANAGE_STATE(AfxGetStaticModuleState()),结果是
_AFXWIN_INLINE HINSTANCE AFXAPI AfxGetResourceHandle()
{ ASSERT(afxCurrentResourceHandle != NULL);
#define afxCurrentResourceHandle AfxGetModuleState()->m_hCurrentResourceHandle
原因是_afxThreadState->m_pModuleState变成了
AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState()
{
AFX_MODULE_STATE* pModuleState = &afxModuleState;
return pModuleState;
}
static _AFX_DLL_MODULE_STATE afxModuleState;
而原本AfxGetModuleState()返回的是_afxBaseModuleState,两者是不同的。现在连资源都装载不了,更别说下面的消息循环了!而前面我们已经能够装载资源了,所以添加的这句话简直是火上浇油,毫无益处!
现在我们就纳闷了,现在的情况跟以前我们遇到的显示DLL里的窗口有什么不同么?为什么之前可以这么做,而现在不可以?看起来两者完全一样嘛!仔细想一想,原来以前的情况是只有一个线程,跨越两个DLL,所以需要切换的只是ModuleState。现在是在同一个DLL/EXE里,需要跨越的是两个线程!哦,这样看来,应该要切换的是ModuleThreadState,让两个线程使用同一个ModuleThreadState。因此,需要线程的创建者把自己的ModuleThreadState传给被创建线程,并进行相关的替换--你可以查看AfxBeginThread/CWinThread的实现代码,而以下是我的简单实现。
DWORD CTestMFCDlg::ThreadFunc(PVOID ref_mts)
{
AFX_MODULE_THREAD_STATE* mts=AfxGetModuleThreadState();
AFX_MODULE_THREAD_STATE backup_mts;
memcpy(&backup_mts,mts,sizeof(AFX_MODULE_THREAD_STATE));
memcpy(mts,(AFX_MODULE_THREAD_STATE*)ref_mts,sizeof(AFX_MODULE_THREAD_STATE));
CAboutDlg dlg;
dlg.DoModal();
memcpy(mts,&backup_mts,sizeof(AFX_MODULE_THREAD_STATE));
return 0;
}
void CTestMFCDlg::OnOK()
{
//AfxBeginThread((AFX_THREADPROC)ThreadFunc,NULL);
::CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)ThreadFunc,AfxGetModuleThreadState(),NULL,NULL);
}
因为线程结束后,线程的私有数据会被删除,所以需要首先保存原有的地址,在最后恢复再删除,否则删除的将是引用的数据。