工作线程与消息循环 blogdown的整理

11 篇文章 0 订阅

   首先声明, 这里的工作线程与UI线程是相对的,即没有任何窗口的. 如果需要与主线程或其它辅助线程通讯,有几种方法如事件,消息,信号等,也可以是以上几种方法的综合运用.下面就列出以下3种通讯方法的代码框架

  (1)只用消息通讯

 1   DWORD ThreadProc(LPVOID lParam)
 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, 00);
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)只用事件通讯 

 1   DWORD ThreadProc(LPVOID lParam)
 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)用消息和事件通讯

 1  DWORD ThreadProc(LPVOID lParam)
 2   {
 3    while (TRUE)
 4   {
 5         DWORD ret ; 
 6         MSG msg ; 
 7   
 8         while (PeekMessage(&msg, NULL, 00, 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;  //定义一个函数指针来指向调用AfxBeginThread的命令所在的类
 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()
{
 // TODO: 在此添加控件通知处理程序代码
        CMyUI *m_pMyUIThread;  //UI线程
 CWinThread *myThread;
 m_pMyUIThread = (CMyUI *) AfxBeginThread(RUNTIME_CLASS(CMyUI));  //开始一个UI线程
 m_pMyUIThread->PostThreadMessage(WM_MYMSG_ONE,NULL,NULL);        //对UI线程发送消息
        myThread = AfxBeginThread((AFX_THREADPROC) ThreadPro,this);      //开始一个工作者线程
 myThread = NULL;
        m_pMyUIThread = NULL;
 
}

ThreadProc.h
UINT ThreadPro(LPVOID wParam);

ThreadProc.cpp
#include "stdafx.h"
#include "ThreadProc.h"
#include "mytest17Dlg.h"
#include "MyUI.h"

UINT ThreadPro(LPVOID wParam)
{
 Cmytest17Dlg *pDlg = (Cmytest17Dlg *) wParam;
 pDlg->m_pMyUIThread->PostThreadMessage(WM_MYMSG_ONE,NULL,NULL);
 AfxEndThread(0);  //执行完在这里结束线程序
 return 1;
}


MyUI.h
pragma once
#define WM_MYMSG_ONE   (WM_USER + 1)  //定义两个消息
#define WM_MYMSG_TWO   (WM_USER + 2)


// CMyUI

class CMyUI : public CWinThread
{
 DECLARE_DYNCREATE(CMyUI)

protected:
 CMyUI();           // 动态创建所使用的受保护的构造函数
 virtual ~CMyUI();

public:
 virtual BOOL InitInstance();
 virtual int ExitInstance();
 afx_msg void MsgOneProc(UINT wParam, LONG lParam);   //定义消息处理函数,头要用afx_msg  参数要用UINT wParam, LONG //lParam
 afx_msg void MsgTwoProc(UINT wParam, LONG lParam);

protected:
 DECLARE_MESSAGE_MAP()
};


MyUI.CPP

#include "stdafx.h"
#include "mytest17.h"
#include "MyUI.h"


// CMyUI

void CMyUI::MsgOneProc(UINT mParam, LONG lParam)   //前面不用加afx_msg
{
 AfxMessageBox("this is message process one",MB_OK);
 return;
}

void CMyUI::MsgTwoProc(UINT mParam, LONG lParam)
{
 AfxMessageBox("this is message process two",MB_OK);
 return;
}

IMPLEMENT_DYNCREATE(CMyUI, CWinThread)

CMyUI::CMyUI()
{
}

CMyUI::~CMyUI()
{
 AfxEndThread(0);
}

BOOL CMyUI::InitInstance()
{
 // TODO: 在此执行任意逐线程初始化
 return TRUE;
}

int CMyUI::ExitInstance()
{
 // TODO: 在此执行任意逐线程清理
 return CWinThread::ExitInstance();
}

BEGIN_MESSAGE_MAP(CMyUI, CWinThread)
 ON_THREAD_MESSAGE(WM_MYMSG_ONE,MsgOneProc)   //消息映射表:前面加ON_THREAD_MESSAGE(消息名,消息处理函数)
 ON_THREAD_MESSAGE(WM_MYMSG





 

遭遇Windows消息循环




今天跟同事联调一段代码,被一个问题郁闷了很久。

调用过程其实并不复杂,就是他提供一个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);

 

 

原来产生AssertCWnd对象是主窗口对象,也就是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对象的情况。这就是为什么我们经常在非MFCCOM组件里可以多线程使用单个MFC窗口的原因。MFC并不是只能在主线程里使用,只是不能跨线程传递对象而以。

 

4.有没有可能使用CreateThread()创建线程,而还能在线程里显示窗口的?特别是有些代码是第三方开发的,你无法修改的时候。当然了,AfxBeginThread内部可能也是调用CreateThread这个WindowsAPI,既然它能,我们肯定也能,只是代码的多少而已。有没有可能像前面的文章对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
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值