MFC4.0的CString在多线程环境下的一点研究

来源:互联网 发布:软件设计师证书查询 编辑:程序博客网 时间:2024/05/10 22:11

CString在MFC下是一个使用非常广泛的字符串类,但是对于其内部实现大部分人却知之甚少。这里简单的描述一下CString在多线程环境下可能碰到的问题。


一般而言,大部分人都认为CString对象并不是线程安全的,在多个线程中访问同一个CString对象需要使用同步手段来保证读写的一致性,而在各自的线程中访问局部的CString对象则没有什么问题。


现在看一下CString的源代码STRCORE.cpp

#define ROUND(x,y) (((x)+(y-1))&~(y-1))#define ROUND4(x) ROUND(x, 4)AFX_STATIC CFixedAlloc _afxAlloc64(ROUND4(65*sizeof(TCHAR)+sizeof(CStringData)));AFX_STATIC CFixedAlloc _afxAlloc128(ROUND4(129*sizeof(TCHAR)+sizeof(CStringData)));AFX_STATIC CFixedAlloc _afxAlloc256(ROUND4(257*sizeof(TCHAR)+sizeof(CStringData)));AFX_STATIC CFixedAlloc _afxAlloc512(ROUND4(513*sizeof(TCHAR)+sizeof(CStringData)));#endif //!_DEBUGvoid CString::AllocBuffer(int nLen)// always allocate one extra character for '\0' termination// assumes [optimistically] that data length will equal allocation length{ASSERT(nLen >= 0);ASSERT(nLen <= INT_MAX-1);    // max size (enough room for 1 extra)if (nLen == 0)Init();else{CStringData* pData;#ifndef _DEBUGif (nLen <= 64){pData = (CStringData*)_afxAlloc64.Alloc();pData->nAllocLength = 64;}else if (nLen <= 128){pData = (CStringData*)_afxAlloc128.Alloc();pData->nAllocLength = 128;}else if (nLen <= 256){pData = (CStringData*)_afxAlloc256.Alloc();pData->nAllocLength = 256;}else if (nLen <= 512){pData = (CStringData*)_afxAlloc512.Alloc();pData->nAllocLength = 512;}else#endif{pData = (CStringData*)new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];pData->nAllocLength = nLen;}pData->nRefs = 1;pData->data()[nLen] = '\0';pData->nDataLength = nLen;m_pchData = pData->data();}}
CString::AllocBuffer是CString为字符串分配内存空间的函数,具体过程Release和Debug有所不同。

在Debug下AllocBuffer直接使用new,得到所需要的空间。而Release下则根据字符串的大小分为1~64,65~256,257~512,512以上四档。前三档都是各自使用一个CFixedAlloc的全局变量进行空间分配。

接着看一下CFixedAlloc类的源码FIXALLOC.CPP

void* CFixedAlloc::Alloc(){EnterCriticalSection(&m_protect);if (m_pNodeFree == NULL){CPlex* pNewBlock = NULL;TRY{// add another blockpNewBlock = CPlex::Create(m_pBlocks, m_nBlockSize, m_nAllocSize);}CATCH_ALL(e){LeaveCriticalSection(&m_protect);THROW_LAST();}END_CATCH_ALL// chain them into free listCNode* pNode = (CNode*)pNewBlock->data();// free in reverse order to make it easier to debug(BYTE*&)pNode += (m_nAllocSize * m_nBlockSize) - m_nAllocSize;for (int i = m_nBlockSize-1; i >= 0; i--, (BYTE*&)pNode -= m_nAllocSize){pNode->pNext = m_pNodeFree;m_pNodeFree = pNode;}}ASSERT(m_pNodeFree != NULL);  // we must have something// remove the first available node from the free listvoid* pNode = m_pNodeFree;m_pNodeFree = m_pNodeFree->pNext;LeaveCriticalSection(&m_protect);return pNode;}
CFixedAlloc::Alloc使用CPlex::Create完成空间申请,不过最显眼的是整个分配过程都在关键代码段内。与之对应的是空间的释放也同样在关键代码段内。在没看到源码前,我怎么也想不到CString会在这个地方使用了关键代码段。这意味着只要使用了CString,各个线程即便毫无关联,也会由此隐晦的联系起来。想像一下这种情况,两个线程都使用了CString对象,由于某种原因,线程A在CString的分配(或者释放)的过程中发生了意外,没有能够调用LeaveCriticalSection,那么线程B在使用CString的时候就会被“意外”的阻塞。


下面就来试验一下这种情况。建立两个线程,各自建立一个CString对象。

DWORD ThreadFunc1(LPVOID lpParam){CString str1;str1 = "abcde";AfxMessageBox("done");return 0;}DWORD ThreadFunc2(LPVOID lpParam){CString str1;str1 = "123456";AfxMessageBox("done");return 0;}void CTestForCStringInMulThreadDlg::OnButtonCreate() {// TODO: Add your control notification handler code herem_hThread1 = (HANDLE)_beginthreadex(NULL,0,(unsigned int (__stdcall *)(void *))ThreadFunc1,NULL,CREATE_SUSPENDED ,NULL);m_hThread2 = (HANDLE)_beginthreadex(NULL,0,(unsigned int (__stdcall *)(void *))ThreadFunc2,NULL,CREATE_SUSPENDED ,NULL);}

接下来的实验需要WinDBG的配合,在线程A进入到关键代码段后将其冻结。

先在线程1上下断点

0:003> x TestForCStringInMulThread!ThreadFunc1004014d0 TestForCStringInMulThread!ThreadFunc1 (void *)0:003> bp 4014d0h
接着恢复线程1

void CTestForCStringInMulThreadDlg::OnButtonRun() {ResumeThread(m_hThread1);}

断下后,在CFixedAlloc::Alloc+0x22上下断点。(CFixedAlloc::Alloc+0x22这里已经进入了关键代码区)

0:001> x mfc42!CFixedAlloc::Alloc73d3198d MFC42!CFixedAlloc::Alloc = <no type information>0:001> bp 73d319AF0:001> g

再次断下后,把线程1冻结。

0:001> ~1 f0:001> g
接着恢复线程2。

void CTestForCStringInMulThreadDlg::OnButtonRun2() {ResumeThread(m_hThread2);}
现在线程2已经被阻塞住了,我们在WinDBG中看一下

0:003> ~* kbn   0  Id: a84.5c8 Suspend: 1 Teb: 7ffdf000 Unfrozen # ChildEBP RetAddr  Args to Child              00 0012fddc 77d191be 77d2776b 0040308c 00000000 ntdll!KiFastSystemCallRet01 0012fe04 73d31233 0040308c 00000000 00000000 USER32!NtUserGetMessage+0xc02 0012fe20 73d46b99 00000004 0012fe8c 0012fe80 MFC42!CWinThread::PumpMessage+0x1503 0012fe44 73d46a2e 00000004 00403058 00403058 MFC42!CWnd::RunModalLoop+0xd904 0012fe80 004010ff 00403058 00402498 00000001 MFC42!CDialog::DoModal+0xe805 0012ff00 73d3cf74 001aa890 00142419 00000000 TestForCStringInMulThread!CTestForCStringInMulThreadApp::InitInstance+0x4f 06 0012ff10 00401aa5 00400000 00000000 00142419 MFC42!AfxWinMain+0x4907 0012ff24 004019ba 00400000 00000000 00142419 TestForCStringInMulThread!WinMain+0x15 [appmodul.cpp @ 30]08 0012ffc0 7c817067 001aa890 0013dd50 7ffd9000 TestForCStringInMulThread!WinMainCRTStartup+0x13409 0012fff0 00000000 00401886 00000000 78746341 kernel32!BaseProcessStart+0x23   1  Id: a84.af8 Suspend: 1 Teb: 7ffde000 Frozen   # ChildEBP RetAddr  Args to Child              00 00d9ff24 73d3269e 00000005 00d9ff74 00388020 MFC42!CFixedAlloc::Alloc+0x2201 00d9ff34 73d3427d 00000005 00000005 00d9ff74 MFC42!CString::AllocBuffer+0x2702 00d9ff44 73d34233 00000005 00403028 00d9ff74 MFC42!CString::AllocBeforeWrite+0x2603 00d9ff54 73d34217 00000005 00403028 00001000 MFC42!CString::AssignCopy+0x1004 00d9ff68 00401505 00403028 73e086d4 00d9ffa4 MFC42!CString::operator=+0x2205 00d9ff80 77c0a3b0 00000000 00001000 00000000 TestForCStringInMulThread!ThreadFunc1+0x35 06 00d9ffb4 7c80b713 00388020 00001000 00000000 msvcrt!_endthreadex+0xa907 00d9ffec 00000000 77c0a341 00388020 00000000 kernel32!BaseThreadStart+0x37   2  Id: a84.2a8 Suspend: 1 Teb: 7ffdd000 Unfrozen # ChildEBP RetAddr  Args to Child              00 00e9fe60 7c92df5a 7c939b23 00000764 00000000 ntdll!KiFastSystemCallRet01 00e9fe64 7c939b23 00000764 00000000 00000000 ntdll!NtWaitForSingleObject+0xc02 00e9feec 7c921046 00e0e578 73d319af 73e0e578 ntdll!RtlpWaitForCriticalSection+0x13203 00e9fef4 73d319af 73e0e578 00000040 00000006 ntdll!RtlEnterCriticalSection+0x4604 00e9ff24 73d3269e 00000006 00e9ff74 003880b0 MFC42!CFixedAlloc::Alloc+0x2205 00e9ff34 73d3427d 00000006 00000006 00e9ff74 MFC42!CString::AllocBuffer+0x2706 00e9ff44 73d34233 00000006 00403030 00e9ff74 MFC42!CString::AllocBeforeWrite+0x2607 00e9ff54 73d34217 00000006 00403030 00000010 MFC42!CString::AssignCopy+0x1008 00e9ff68 00401575 00403030 73e086d4 00e9ffa4 MFC42!CString::operator=+0x2209 00e9ff80 77c0a3b0 00000000 00000010 0012f574 TestForCStringInMulThread!ThreadFunc2+0x35 0a 00e9ffb4 7c80b713 003880b0 00000010 0012f574 msvcrt!_endthreadex+0xa90b 00e9ffec 00000000 77c0a341 003880b0 00000000 kernel32!BaseThreadStart+0x37

可以很清楚的看到线程2已经被阻塞住了。