MFC中CAsyncSocket及其派生类对象跨线程使用方法
存在的现象:
在MFC中用多线程方法开发WinSocket通讯程序时,如果你的的是API方式,自然没有以下说的问题。但如果当你使用CAsyncSocket及其派生类(CSocket或是你自己的写的)来开发的话,会发现在不同线程中使用CAsyncSocket及其派生类对象时,会出现程序崩溃。这里所说的跨线程,是指该对象在一个线程中调用 Create/Close/Attach/Detach函数,然后在另外一个线程中调用其他成员函数如Receive/ReceiveFrom/Send /SendTo等。下面是一个典型的导致崩溃的例子程序(片断):
CAsyncSocketasync_socket;
UINTThreadProc(LPVOID) //出错:1
{
TRACE("======== ThreadDeattch出错:1 ========\n");
async_socket.Close (); //错误发生在这个调用上:要进行关闭async_socket对象.
return 0;
}
voidCTsAsncSocketDlg::OnBnClickedButtonCreate() //出错:1
{
async_socket.Create(0);//async_socket对象在主线程中被建立
::AfxBeginThread(ThreadProc,0,0,0,0,0); //async_socket对象在主线程中被传递到另外的线程:ThreadProc
}
要想知道错误的原因,可以跟进去分析。这样说起来又太繁琐了,网上有不少类似的文章,问题的关键是不要在不同线程中进行Create/Close调用。
首先,当你的程序有问题的时候,怎样来判断是因为这个问题?这是关键一步。调试/断点/观测等有很多手段,VC++的IDE在个这个方面是非常出色的。这里还有个简单的方法。比如有如下代码:
classCSocketP3PServer : public CAsyncSocket
{
public:
CSocketP3PServer ();
~ CSocketP3PServer ();
public:
.......... //others
#if _DEBUG
private:
DWORD m_sockCurrentThreadId;
#endif //_DEUBG
};
voidCSocketP3PServer::CSocketP3PServer()
{
#if _DEBUG
m_sockCurrentThreadId= ::GetCurrentThreadId();
#endif //_DEBUG
}
CSocketP3PServer::~CSocketP3PServer()
{
#if _DEBUG
if(m_sockCurrentThreadId != ::GetCurrentThreadId())
{
//如果程序走到这里来了,那么应该是跨线程使用造成的问题
TRACE("======= CSocketP3PServer 对象(Address:%d)在不同线程调用了 ========\n",this);
}
#endif //_DEBUG
}
知道了问题所在,解决的办法就很好说了,网上也有很多相关的介绍文章。这里是把这些解决方法整理一下。
解决的思路主要有2条:
1.避免这个问题的出现,也就是说避免在跨线程中使用CAsyncSocket及其派生类对象。关于这点有很多的招数,比如说可以 A线程专门作收发工作,而其它线程把相关的数据通过数据共享或是其它手段在彼此之间传递。这个就是一个<读者-作者>之间的关系。还有其它的办法,总之,就是要把socket对象整个生命期都在单一的线程上。
2.实在是没有办法避免不在跨线程中使用CAsyncSocket及其派生类对象的,可以通过在线程之间传递SOCKET <变量>来实现,这里给个例子,代码如下:
UINTThreadDeattchMe(LPVOID pv) //正确的在跨线程使用as_socket,这里是工作线程A
{
TRACE("======== ThreadDeattch 正确的在跨线程使用SOCKET ========\n");
as_socket.Attach((SOCKET)pv); //<---------- 注意这里传递的是 SOCKET <变量>
as_socket.Close ();
return 0;
}
voidCTsAsncSocketDlg::OnBnClickedButtonCreate()//正确的在跨线程使用SOCKET,这里是主线程
{
as_socket.Create(0);
AfxBeginThread(ThreadDeattchMe,(LPVOID)as_socket.Detach(),0,0,0,0);//<----------注意这里传递的是SOCKET <变量>
}
另外一个解释:
现象
用多线程方法设计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 notifywindow!\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 PASCALCAsyncSocket::KillSocket(SOCKET hSocket, CAsyncSocket* pSocket)
{
ASSERT(CAsyncSocket::LookupHandle(hSocket, FALSE) !=NULL);
我们在这个ASSERT处设置断点,跟踪进LookupHandle,会发现这个函数定义如下:
CAsyncSocket* PASCAL CAsyncSocket::LookupHandle(SOCKEThSocket, 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的时候这么做,因为这增加了出错的风险(尤其当出现拆解包问题时,有人称为粘包,我基本不认同这种称呼)。如果一定要这么做,方法应该是:
上面的例子,我稍微做修改,就不会出错了:
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);
}