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 //!_DEBUG
void 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 _DEBUG
if (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 block
pNewBlock = CPlex::Create(m_pBlocks, m_nBlockSize, m_nAllocSize);
}
CATCH_ALL(e)
{
LeaveCriticalSection(&m_protect);
THROW_LAST();
}
END_CATCH_ALL
// chain them into free list
CNode* 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 list
void* 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 here
m_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!ThreadFunc1
004014d0 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::Alloc
73d3198d MFC42!CFixedAlloc::Alloc = <no type information>
0:001> bp 73d319AF
0:001> g
再次断下后,把线程1冻结。
0:001> ~1 f
0: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!KiFastSystemCallRet
01 0012fe04 73d31233 0040308c 00000000 00000000 USER32!NtUserGetMessage+0xc
02 0012fe20 73d46b99 00000004 0012fe8c 0012fe80 MFC42!CWinThread::PumpMessage+0x15
03 0012fe44 73d46a2e 00000004 00403058 00403058 MFC42!CWnd::RunModalLoop+0xd9
04 0012fe80 004010ff 00403058 00402498 00000001 MFC42!CDialog::DoModal+0xe8
05 0012ff00 73d3cf74 001aa890 00142419 00000000 TestForCStringInMulThread!CTestForCStringInMulThreadApp::InitInstance+0x4f
06 0012ff10 00401aa5 00400000 00000000 00142419 MFC42!AfxWinMain+0x49
07 0012ff24 004019ba 00400000 00000000 00142419 TestForCStringInMulThread!WinMain+0x15 [appmodul.cpp @ 30]
08 0012ffc0 7c817067 001aa890 0013dd50 7ffd9000 TestForCStringInMulThread!WinMainCRTStartup+0x134
09 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+0x22
01 00d9ff34 73d3427d 00000005 00000005 00d9ff74 MFC42!CString::AllocBuffer+0x27
02 00d9ff44 73d34233 00000005 00403028 00d9ff74 MFC42!CString::AllocBeforeWrite+0x26
03 00d9ff54 73d34217 00000005 00403028 00001000 MFC42!CString::AssignCopy+0x10
04 00d9ff68 00401505 00403028 73e086d4 00d9ffa4 MFC42!CString::operator=+0x22
05 00d9ff80 77c0a3b0 00000000 00001000 00000000 TestForCStringInMulThread!ThreadFunc1+0x35
06 00d9ffb4 7c80b713 00388020 00001000 00000000 msvcrt!_endthreadex+0xa9
07 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!KiFastSystemCallRet
01 00e9fe64 7c939b23 00000764 00000000 00000000 ntdll!NtWaitForSingleObject+0xc
02 00e9feec 7c921046 00e0e578 73d319af 73e0e578 ntdll!RtlpWaitForCriticalSection+0x132
03 00e9fef4 73d319af 73e0e578 00000040 00000006 ntdll!RtlEnterCriticalSection+0x46
04 00e9ff24 73d3269e 00000006 00e9ff74 003880b0 MFC42!CFixedAlloc::Alloc+0x22
05 00e9ff34 73d3427d 00000006 00000006 00e9ff74 MFC42!CString::AllocBuffer+0x27
06 00e9ff44 73d34233 00000006 00403030 00e9ff74 MFC42!CString::AllocBeforeWrite+0x26
07 00e9ff54 73d34217 00000006 00403030 00000010 MFC42!CString::AssignCopy+0x10
08 00e9ff68 00401575 00403030 73e086d4 00e9ffa4 MFC42!CString::operator=+0x22
09 00e9ff80 77c0a3b0 00000000 00000010 0012f574 TestForCStringInMulThread!ThreadFunc2+0x35
0a 00e9ffb4 7c80b713 003880b0 00000010 0012f574 msvcrt!_endthreadex+0xa9
0b 00e9ffec 00000000 77c0a341 003880b0 00000000 kernel32!BaseThreadStart+0x37
可以很清楚的看到线程2已经被阻塞住了。