CAsyncSocket對象不能跨線程之分析
現(xiàn)象
用多線程方法設(shè)計socket程序時,你會發(fā)現(xiàn)在跨線程使用CAsyncSocket及其派生類時,會出現(xiàn)程序崩潰。所謂跨線程,是指該對象在一個線程中調(diào)用Create/AttachHandle/Attach函數(shù),然后在另外一個線程中調(diào)用其他成員函數(shù)。下面的例子就是一個典型的導致崩潰的過程: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對象在主線程中被調(diào)用,在子線程中被關(guān)閉。
跟蹤分析
這個問題的原因可以通過單步跟蹤(F11)的方法來了解。我們在Socket.Create(0)處設(shè)斷點,跟蹤進去會發(fā)現(xiàn)下面的函數(shù)被調(diào)用:
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); }
在這個函數(shù)的開頭,首先獲得了一個pState的指針指向_afxSockThreadState對象。從名字可以看出,這似乎是一個和線程相關(guān)的變量,實際上它是一個宏,定義如下:
#define _afxSockThreadState AfxGetModuleThreadState()
我們沒有必要去細究這個指針的定義是如何的,只要知道它是和當前線程密切關(guān)聯(lián)的,其他線程應該也有類似的指針,只是指向不同的結(jié)構(gòu)。
在這個函數(shù)中,CAsyncSocket創(chuàng)建了一個窗口,并把如下兩個信息加入到pState所管理的結(jié)構(gòu)中:
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);
當調(diào)用Close時,我們再次跟蹤,就會發(fā)現(xiàn)在KillSocket中,下面的函數(shù)出現(xiàn)錯誤:
void PASCAL CAsyncSocket::KillSocket(SOCKET hSocket, CAsyncSocket* pSocket) { ASSERT(CAsyncSocket::LookupHandle(hSocket, FALSE) != NULL);
我們在這個ASSERT處設(shè)置斷點,跟蹤進LookupHandle,會發(fā)現(xiàn)這個函數(shù)定義如下:
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; }
顯然,這個函數(shù)試圖從當前線程查詢關(guān)于這個 socket的信息,可是這個信息放在創(chuàng)建這個socket的線程中,因此這種查詢顯然會失敗,最終返回NULL。
有人會問,既然它是ASSERT出錯,是不是Release就沒問題了。這只是自欺欺人。ASSERT/VERIFY都是檢驗一些程序正常運行必須正確的條件。如果ASSERT都失敗,在Release中也許不會顯現(xiàn),但是你的程序肯定運行不正確,啥時候出錯就不知道了。
如何在多線程之間傳遞socket
有些特殊情況下,可能需要在不同線程之間傳遞socket。當然我不建議在使用CAsyncSOcket的時候這么做,因為這增加了出錯的風險(尤其當出現(xiàn)拆解包問題時,有人稱為粘包,我基本不認同這種稱呼)。如果一定要這么做,方法應該是:
- 當前擁有這個socket的線程調(diào)用Detach方法,這樣socket句柄和C++對象及當前線程脫離關(guān)系
- 當前線程把這個對象傳遞給另外一個線程
- 另外一個線程創(chuàng)建新的CAsyncSocket對象,并調(diào)用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); }
from: http://blog.vckbase.com/arong/archive/2005/12/03/15578.html
posted on 2006-11-07 20:07 weidagang2046 閱讀(487) 評論(0) 編輯 收藏 所屬分類: Windows