arongnet 阅读(947) 评论(13)

现象

用多线程方法设计socket程序时,你会发现在跨线程使用CAsyncSocket及其派生类时,会出现程序崩溃。所谓跨线程,是指该对象在一个线程中调用Create/AttachHandle/Attach函数,然后在另外一个线程中调用其他成员函数。下面的例子就是一个典型的导致崩溃的过程:
CAsyncSocket Socket;
UINT Thread(LPVOID)
{
       Socket.Close ();
       return 0;
}
void CTestSDlg::OnOK() 
{
       // TODO: Add extra validation here
       Socket.Create(0);
       AfxBeginThread(Thread,0,0,0,0,0);
}

其中Socket对象在主线程中被调用,在子线程中被关闭。

跟踪分析

这个问题的原因可以通过单步跟踪(F11)的方法来了解。我们在Socket.Create(0)处设断点,跟踪进去会发现下面的函数被调用:

void PASCAL CAsyncSocket::AttachHandle(
          SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead)
{
    _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
    BOOL bEnable = AfxEnableMemoryTracking(FALSE);
    if (!bDead)
    {
             ASSERT(CAsyncSocket::LookupHandle(hSocket, bDead) == NULL);
             if (pState->m_pmapSocketHandle->IsEmpty())
             {
                  ASSERT(pState->m_pmapDeadSockets->IsEmpty());
                  ASSERT(pState->m_hSocketWindow == NULL);
                  CSocketWnd* pWnd = new CSocketWnd;
                  pWnd->m_hWnd = NULL;
                  if (!pWnd->CreateEx(0, AfxRegisterWndClass(0),
                                   _T("Socket Notification Sink"),
                                 WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL))
                 {
                       TRACE0("Warning: unable to create socket notify window!\n");
                       AfxThrowResourceException();
                 }
                 ASSERT(pWnd->m_hWnd != NULL);
                 ASSERT(CWnd::FromHandlePermanent(pWnd->m_hWnd) == pWnd);
                 pState->m_hSocketWindow = pWnd->m_hWnd;
            }
            pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
    }
    else
    {
           int nCount;
           if (pState->m_pmapDeadSockets->Lookup((void*)hSocket, (void*&)nCount))
                     nCount++;
           else
                     nCount = 1;
           pState->m_pmapDeadSockets->SetAt((void*)hSocket, (void*)nCount);
   }
   AfxEnableMemoryTracking(bEnable);
}

在这个函数的开头,首先获得了一个pState的指针指向_afxSockThreadState对象。从名字可以看出,这似乎是一个和线程相关的变量,实际上它是一个宏,定义如下:

#define _afxSockThreadState AfxGetModuleThreadState()

我们没有必要去细究这个指针的定义是如何的,只要知道它是和当前线程密切关联的,其他线程应该也有类似的指针,只是指向不同的结构。

在这个函数中,CAsyncSocket创建了一个窗口,并把如下两个信息加入到pState所管理的结构中:

    pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
    pState->m_pmapDeadSockets->SetAt((void*)hSocket, (void*)nCount);
    pState->m_hSocketWindow = pWnd->m_hWnd;
    pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);

当调用Close时,我们再次跟踪,就会发现在KillSocket中,下面的函数出现错误:

    void PASCAL CAsyncSocket::KillSocket(SOCKET hSocket, CAsyncSocket* pSocket)
    {
            ASSERT(CAsyncSocket::LookupHandle(hSocket, FALSE) != NULL);

我们在这个ASSERT处设置断点,跟踪进LookupHandle,会发现这个函数定义如下:

CAsyncSocket* PASCAL CAsyncSocket::LookupHandle(SOCKET hSocket, BOOL bDead)
{
     CAsyncSocket* pSocket;
     _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
     if (!bDead)
     {
             pSocket = (CAsyncSocket*)
             pState->m_pmapSocketHandle->GetValueAt((void*)hSocket);
             if (pSocket != NULL)
                  return pSocket;
    }
    else
    {
             pSocket = (CAsyncSocket*)
                  pState->m_pmapDeadSockets->GetValueAt((void*)hSocket);
             if (pSocket != NULL)
                   return pSocket;
    }
    return NULL;
}

显然,这个函数试图从当前线程查询关于这个 socket的信息,可是这个信息放在创建这个socket的线程中,因此这种查询显然会失败,最终返回NULL。

有人会问,既然它是ASSERT出错,是不是Release就没问题了。这只是自欺欺人。ASSERT/VERIFY都是检验一些程序正常运行必须正确的条件。如果ASSERT都失败,在Release中也许不会显现,但是你的程序肯定运行不正确,啥时候出错就不知道了。

如何在多线程之间传递socket

有些特殊情况下,可能需要在不同线程之间传递socket。当然我不建议在使用CAsyncSOcket的时候这么做,因为这增加了出错的风险(尤其当出现拆解包问题时,有人称为粘包,我基本不认同这种称呼)。如果一定要这么做,方法应该是:

  1. 当前拥有这个socket的线程调用Detach方法,这样socket句柄和C++对象及当前线程脱离关系
  2. 当前线程把这个对象传递给另外一个线程
  3. 另外一个线程创建新的CAsyncSocket对象,并调用Attach

上面的例子,我稍微做修改,就不会出错了:

CAsyncSocket Socket;
UINT Thread(LPVOID sock)
{
         Socket.Attach((SOCKET)sock);
         Socket.Close ();
         return 0;
}
void CTestSDlg::OnOK() 
{
         // TODO: Add extra validation here
         Socket.Create(0);
         SOCKET hSocket = Socket.Detach ();
         AfxBeginThread(Thread,(LPVOID)hSocket,0,0,0,0);
}

评论列表
ocean
re: CAsyncSocket对象不能跨线程之分析
很好!
babazhang
re: CAsyncSocket对象不能跨线程之分析
MSDN里给出的是一句不痛不痒的话,CSocket /CAsyncSocket is not thread-safety.  说白了就是数据一旦跟窗口搭上钩后,消息循化就会和子线程有数据竞争,而socket缓冲区是没有采用同步对象保护的,所以就会不安全。
Shen Fang
re: CAsyncSocket对象不能跨线程之分析
很实用,非常感谢搂主
lele
re: CAsyncSocket对象不能跨线程之分析
我用了一下,不能通过,在attach处出错
libo
re: CAsyncSocket对象不能跨线程之分析
说的很好,感谢。
玻璃小屋
re: CAsyncSocket对象不能跨线程之分析

荣哥!!!顶!!
刘庭洋
re: CAsyncSocket对象不能跨线程之分析
十分感谢,我正需要使用CAsyncSocket跨线程操作。
aayz
re: CAsyncSocket对象不能跨线程之分析
感谢
michaelee
re: CAsyncSocket对象不能跨线程之分析
我把CAsyncSocket Socket作为CTestSDlg的一个public成员,把Thread函数作为CTestSDlg的一个静态公共成员函数。然后在传递线程参数的时候,把把this指针传给线程函数。
把你的代码修改一下:

UINT  CTestSDlg::Thread(LPVOID param)
{
 CTestSDlg *pDlg = ( CTestSDlg*)param;
       pDlg->Socket.Close ();
       return 0;
}
void CTestSDlg::OnOK() 
{
       // TODO: Add extra validation here
       Socket.Create(0);
       AfxBeginThread(Thread,LPVOID)this,0,0,0,0);
}

这样子会有问题吗,我现在做的东西就是这样子的。
不知道会不会出错。
小马_xiao
re: CAsyncSocket对象不能跨线程之分析
这样只是 把CSocket 从一个线程搬到令一个线程 ,能实现多线程共享吗

发表评论
切换编辑模式