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

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已经被阻塞住了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值