第一篇里说了使用AFX_MANAGE_STATE(AfxGetStaticModuleState())进行DLL间的资源切换,以及工作线程中创建Windows消息循环的原理,以为就可以搞定一切类似问题了…但是请看以下代码
 
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);
}
 
因为线程结束后,线程的私有数据会被删除,所以需要首先保存原有的地址,在最后恢复再删除,否则删除的将是引用的数据。

解决方案 »

  1.   

    简单的话就一句:domoal就ok了,然后就创建了对话框
      

  2.   

    http://topic.csdn.net/u/20100115/20/3b8fccad-2b06-4797-abc3-41528b6ca8bd.html?810
      

  3.   

         不错,以前我也研究过如何在工作线程中弹出对话框的原理,不过没有搂主这么深入。下面谈谈我的一些看法:    事实上MFC在多线程中大量运用了TLS(线程本地存储)来保存某些状态,所以在多个线程中使用同一个CWnd类继承的C++对象(也就是说窗口对象)是非常危险的,解决办法有两个关键点:1、使用AfxBeginThread而不是CreateThread来创建线程,能够维持正确的MFC状态;2、线程间避免直接传递并使用CWnd-based对象的指针,而应该传递HWND。如果需要使用CWnd的高级封装特性,就要调用 CWnd::FromHandle()产生一个临时的C++对象来操作窗口句柄,但此时很多用到了TLS的成员函数是不能使用的,否则结果跟直接传递 CWnd指针是一样的。     线程间传递对象的问题,注意下面一段英文的时候那个“In general”,
    // 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.      实际上线程间是可以传递对象指针的。线程间传递对象主要需要注意两方面问题:一是窗口属于创建它的线程,有少数功能只有在窗口所属线程中才能执行;二是多个线程访问同一对象要考虑线程同步。凡是通过SendMessage来实现的功能都可以直接使用,因为SendMessage是把消息发给窗口所属线程来处理的。由于很多人对此不是很清楚,所以建议初学者用传递句柄的方式代替传递对象指针,通过消息让窗口所属线程来处理。 
      

  4.   

    楼主分析的不错,以前只知道有人说不能在线程中传递MFC对像,现在知道了
      

  5.   

    在线程中使用非模态对话框,然后使用一列表来维护此对话框,关闭窗口后有时就出现楼主所说的ASSERT错误,有时还会莫名其妙的退出,现在知道部分原因了,感谢!!!
      

  6.   

    _AFX_BASE_MODULE_STATE 
    是干啥的?