线程访问窗口资源的问题

时常为所开的线程传入个窗口参数 (cwnd),这里里面有个很大的隐患就是当做CWnd::AssertValid()检查,就会遇到断言失败。具体原因在于MFC管理窗口的机制。MFC有个窗口句柄和窗口类对象的映射表,就是Windows对象(句柄)和C++对象的一个映射,它是在窗口调用CWnd::Create(),CWnd::CreateEx()时,就添加到映射表里的。映射表的一个重要的特征,它只存于一个线程当中。当你在B线程使用A线程的窗口对象指针pWnd,B线程对此对象指针做AssertValid时,会因为以下几种原因导致断言失败:

1.  CHandleMap*   pMap   =   afxMapHWND();  ASSERT(pMap != NULL)失败;该线程没有映射表。原因这个线程只是工作线程(没有主窗口),并不是用户界面线程(利用界面线程可以在一个进程的地址空间开启几个并行运行的窗口)。在工作线程里调用AfxGetMainWnd()->m_hWnd出错,也会发生发生内存访问冲突。(AfxGetMainWnd得到的是当前线程的主窗口)。

2.ASSERT((p = pMap->LookupPermanent(m_hWnd)) != NULL ||
   (p = pMap->LookupTemporary(m_hWnd)) != NULL)失败;从线程的句柄映射表没有句柄pWnd->m_hWnd所对应  的对象。

3.ASSERT((CWnd*)p == this);句柄pWnd->m_hWnd所对应  的对象并不是自身。

进行这几种断言的目的:确保C++窗口对象(Cwnd)与窗口本身(句柄HWND)间的一致性(一一对应),并不因为只要修改一个Cwnd对象中的m_hwnd值就可改变Cwnd与HWND间的映射关系(合法的修改途径:映射可以被Cwnd成员函数Detach,Attach所改变),再者可以确保使用窗口的安全性,避免出现访问违规等状况,如使用临时对象就很容易出现这种断言失败。

每个界面线程都有自身的映射表,TLS(线程局部存储)。映射表有两类:永久的,临时的。.普通窗口,如程序主窗口的映射关系在永久映射表中。并不是所有的句柄都有个Cwnd对象对其进行封装,所以如果无法在永久映射中找到HWND对应的CWnd对象,就会创建一个临时CWnd对象包装此HWND,并将它们记入临时映射表中.当程序空闲时(OnIdle()),会删除所创的临时窗口对象.此后对该CWnd的引用都将引发上述的断言出错(状况2,3). 也是因这个原因,GetDlgItem()等常用函数返回的都是临时对象,不能在程序中保存以在以后使用,而应该随用随取.注意MSDN中的说明:The   returned   pointer   may   be   temporary   and   should   not   be   stored   for   later   use.

下面是断言源码:

void CWnd::AssertValid() const
{
 if (m_hWnd == NULL)
  return;     // null (unattached) windows are valid

 // check for special wnd??? values
 ASSERT(HWND_TOP == NULL);       // same as desktop
 if (m_hWnd == HWND_BOTTOM)
  ASSERT(this == &CWnd::wndBottom);
 else if (m_hWnd == HWND_TOPMOST)
  ASSERT(this == &CWnd::wndTopMost);
 else if (m_hWnd == HWND_NOTOPMOST)
  ASSERT(this == &CWnd::wndNoTopMost);
 else
 {
  // should be a normal window
  ASSERT(::IsWindow(m_hWnd));

  // should also be in the permanent or temporary handle map
  CHandleMap* pMap = afxMapHWND();
  ASSERT(pMap != NULL);

  CObject* p;
  ASSERT((p = pMap->LookupPermanent(m_hWnd)) != NULL ||
   (p = pMap->LookupTemporary(m_hWnd)) != NULL);
  ASSERT((CWnd*)p == this);   // must be us

  // 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类的指针,是一个常识性的错误。确实是个错误,但可以回避它。只要调用者明白传递这窗口指针的意义,如有时我们传递一个继承的MFC类指针,只是为了得到我们在其中所加入的结构或进行一些与窗口无关的操作(进行窗口操作就会出现上述问题了,因为afxMapHWND得到的是线程本身的句柄表)。如果非要在线程里做一些非本线程窗口的操作,只有创建线程的时候将窗口句柄作为参数传递进去了。其实这是不必要的,我们可以通过发送消息完成线程间窗口的交互。

afxMapHWND源码:

CHandleMap* PASCAL afxMapHWND(BOOL bCreate)
{
        AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState(); //这个函数名就说明句柄和对象的映射表是和线程关联的,也就是在那个线程创建的对象,就放到那个线程的映射表里
        if (pState->m_pmapHWND == NULL && bCreate)
        {
                BOOL bEnable = AfxEnableMemoryTracking(FALSE);
#ifndef _AFX_PORTABLE
                _PNH pnhOldHandler = AfxSetNewHandler(&AfxCriticalNewHandler);
#endif
                pState->m_pmapHWND = new CHandleMap(RUNTIME_CLASS(CTempWnd),
                        offsetof(CWnd, m_hWnd));//创建句柄映射表,存到AFX_MODULE_THREAD_STATE的成员。
//AfxGetModuleThreadState是用线程局部存储实现.
#ifndef _AFX_PORTABLE
                AfxSetNewHandler(pnhOldHandler);
#endif
                AfxEnableMemoryTracking(bEnable);
        }
        return pState->m_pmapHWND;
}

 

线程 访问 窗口资源

分享到:
评论加载中,请稍后...
创APP如搭积木 - 创意无限,梦想即时!
回到顶部