ATL OLEDB 内存泄漏 2:CDynamicParameterAccessor
CDynamicParameterAccessor 在特殊情况下存在微妙的内存泄漏问题
引言
本文揭示了 CDynamicParameterAccessor
ATL OLEDB 对象中一个棘手的内存泄漏错误。本文解释了发生这种情况的原因,并提供了解决方案。这是之前关于 CCommand
的内存泄漏文章的延续。与 CCommand
相比,此错误泄漏的内存量较小,并且仅在特殊情况下才会发生。但是,如果 24/7 服务器代码以这种方式泄漏内存,它迟早会耗尽内存。
背景
Microsoft Knowledge Base 描述了一个众所周知的内存泄漏问题以及修复方法。但是,还有另一个微妙的内存泄漏问题,在任何地方都未见报告。最有可能的是,没有人以这种方式使用 ATL OLEDB。但是,在我的项目中,这是一个典型的使用场景,因为性能至关重要。
谁会像本例中那样运行同一命令一百万次?我会。这个想法是准备带有参数的命令,并使用不同的参数运行它。我们用它来编写交易订单和其他东西,在白天实时写入 SQL Server。这样,我们将获得最佳性能,因为您无需生成 SQL 字符串并一遍又一遍地准备它。
CDataSource ds;//Create and open the data source
CDBPropSet dbinit(DBPROPSET_DBINIT);
dbinit.AddProperty(DBPROP_INIT_DATASOURCE, "192.168.60.18");
dbinit.AddProperty(DBPROP_AUTH_USERID, "user");
dbinit.AddProperty(DBPROP_AUTH_PASSWORD, "");
dbinit.AddProperty(DBPROP_INIT_CATALOG, "master");
dbinit.AddProperty(DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO, false);
dbinit.AddProperty(DBPROP_INIT_LCID, 1033L);
dbinit.AddProperty(DBPROP_INIT_PROMPT, static_cast<short>(4));
HRESULT hr = ds.Open(_T("SQLOLEDB.1"), &dbinit);//Create Session.
CSession session;
session.Open(ds);
CCommand<CDynamicParameterAccessor, CRowset, CMultipleResults> command;
hr = command.Create(session, "exec sp_tables");
for(int i=0; i<1000000; i++ )
{
hr = command.Open(NULL,NULL,false,0);
command.Close();
if( command.GetMultiplePtr() != NULL )
{
command.GetMultiplePtr()->Release();
*command.GetMultiplePtrAddress() = NULL;
}
}
如果您将代码复制到您的项目中并运行它(请为您的数据库环境使用正确的用户名和密码),您会在任务管理器中发现内存使用量以每秒 10K 的速度增加,尽管另一篇文章中提到的主要内存泄漏已在此处修复。这里的区别在于使用 CDynamicParameterAccessor
而不是 CDynamicAccessor
。并且这个小的内存泄漏发生在 CDynamicParameterAccessor
上,当不使用任何参数时。显然,如果您非常小心并在不需要参数时使用 CDynamicAccessor
,并在存储过程有一些参数时使用 CDynamicParameterAccessor
,您可以解决此错误。但是,在某些情况下,您不想区分它们,因为您可能会将命令包装到一个新类中,并且无论存储过程是否具有参数,都想使用它。
该错误
CCommand::Open()
调用 CDynamicParameterAccessor::BindParameters()
。这是 CDynamicParameterAccessor::BindParameters()
的实现
HRESULT BindParameters(HACCESSOR* pHAccessor, ICommand* pCommand,
void** ppParameterBuffer, bool fBindLength = false, bool fBindStatus = false ) throw()
{
// If we have already bound the parameters then just return
// the pointer to the parameter buffer
if (*pHAccessor != NULL)
{
*ppParameterBuffer = m_pParameterBuffer;
return S_OK;
}
CComPtr<IAccessor> spAccessor;
ATLENSURE_RETURN(pCommand != NULL);
HRESULT hr = pCommand->QueryInterface(&spAccessor);
if (FAILED(hr))
return hr;
// Try to bind parameters if available
CComPtr<ICommandWithParameters> spCommandParameters;
hr = pCommand->QueryInterface(&spCommandParameters);
if (FAILED(hr))
return hr;
DB_UPARAMS ulParams = 0;
CComHeapPtr<DBPARAMINFO> spParamInfo;
LPOLESTR pNamesBuffer;
// Get Parameter Information
hr = spCommandParameters->GetParameterInfo(&ulParams, &spParamInfo,&pNamesBuffer);
if (FAILED(hr))
return hr;
// Create the parameter information for binding
hr = AllocateParameterInfo(ulParams);
if (FAILED(hr))
{
CoTaskMemFree(pNamesBuffer);
return hr;
}
DBBYTEOFFSET nOffset = 0;
DBBYTEOFFSET nDataOffset = 0;
DBBYTEOFFSET nLengthOffset = 0;
DBBYTEOFFSET nStatusOffset = 0;
DBBINDING* pCurrent = m_pParameterEntry;
for (ULONG l=0; l<ulParams; l++)
{
m_pParameterEntry[l].eParamIO = 0;
if (spParamInfo[l].dwFlags & DBPARAMFLAGS_ISINPUT)
m_pParameterEntry[l].eParamIO |= DBPARAMIO_INPUT;
if (spParamInfo[l].dwFlags & DBPARAMFLAGS_ISOUTPUT)
m_pParameterEntry[l].eParamIO |= DBPARAMIO_OUTPUT;
// if this is a BLOB, truncate column length to m_nBlobSize (like 8000 bytes)
if( spParamInfo[l].ulParamSize > m_nBlobSize )
spParamInfo[l].ulParamSize = m_nBlobSize;
// if this is a string, recalculate column size in bytes
DBLENGTH colLength = spParamInfo[l].ulParamSize;
if (spParamInfo[l].wType == DBTYPE_STR)
colLength += 1;
if (spParamInfo[l].wType == DBTYPE_WSTR)
colLength = colLength*2 + 2;
// Calculate the column data offset
nDataOffset = AlignAndIncrementOffset
( nOffset, colLength, GetAlignment( spParamInfo[l].wType ) );
if( fBindLength )
{
// Calculate the column length offset
nLengthOffset = AlignAndIncrementOffset
( nOffset, sizeof(DBLENGTH), __alignof(DBLENGTH) );
}
if( fBindStatus )
{
// Calculate the column status offset
nStatusOffset = AlignAndIncrementOffset
( nOffset, sizeof(DBSTATUS), __alignof(DBSTATUS) );
}
Bind(pCurrent, spParamInfo[l].iOrdinal, spParamInfo[l].wType,
colLength, spParamInfo[l].bPrecision, spParamInfo[l].bScale,
m_pParameterEntry[l].eParamIO, nDataOffset, nLengthOffset, nStatusOffset );
pCurrent++;
m_ppParamName[l] = pNamesBuffer;
if (pNamesBuffer && *pNamesBuffer)
{
// Search for the NULL termination character
while (*pNamesBuffer++)
;
}
}
// Allocate memory for the new buffer
m_pParameterBuffer = NULL;
ATLTRY(m_pParameterBuffer = new BYTE[nOffset]);
if (m_pParameterBuffer == NULL)
{
// Note that pNamesBuffer will be freed in the destructor
// by freeing *m_ppParamName
return E_OUTOFMEMORY;
}
*ppParameterBuffer = m_pParameterBuffer;
m_nParameterBufferSize = nOffset;
m_nParams = ulParams;
BindEntries(m_pParameterEntry, ulParams, pHAccessor, nOffset, spAccessor);
return S_OK;
}
粗体行是问题的根源。
if (*pHAccessor != NULL)
{
*ppParameterBuffer = m_pParameterBuffer;
return S_OK;
}
和
BindEntries(m_pParameterEntry, ulParams, pHAccessor, nOffset, spAccessor);
代码检查 *pHAccessor
是否为 NULL
。如果不是 NULL
,则意味着参数已绑定,无需分配参数相关的内存并绑定参数访问器。这通常很好。唯一的问题是,当存储过程没有参数时,BindEntries()
会失败。在这种情况下,*pHAccessor
仍然是 NULL
,但所有参数相关的内存都已分配。是的,因为 ulParams
为 0
,因为存储过程不需要任何参数。但是,AllocateParameterInfo()
将 new DBBINDING[0]
和 new OLECHAR*[0]
。这些操作实际上分配了一个很好的指针,内容长度为 0
。在发布模式下,两者都消耗 16 字节的内存。在调试版本中,64 字节。由于每次 *pHAccessor
为 NULL
,每次调用时都会分配这小块内存。只有上次分配的内存实际上在析构函数中被释放:~CDynamicParameterAccessor()
。
~CDynamicParameterAccessor()
{
delete [] m_pParameterEntry;
if (m_ppParamName != NULL)
{
CoTaskMemFree(*m_ppParamName);
delete [] m_ppParamName;
}
delete m_pParameterBuffer;
};
解决方案
直接检查内存而不是检查访问器。为了避免直接修改 ATL 代码,我们需要子类化 CDynamicParameterAccessor
class CDynamicParameterAccessorEx : public CDynamicParameterAccessor
{
public:
HRESULT BindParameters(HACCESSOR* pHAccessor, ICommand* pCommand,
void** ppParameterBuffer, bool fBindLength = false,
bool fBindStatus = false) throw()
{
if( NULL == m_pParameterBuffer )
{
return CDynamicParameterAccessor::BindParameters
(pHAccessor, pCommand, ppParameterBuffer, fBindLength, fBindStatus);
}
else
{
*ppParameterBuffer = m_pParameterBuffer;
return S_OK;
}
}
}
然后您继承自 CCommand<CDynamicParameterAccessorEx, CRowset, CMultipleResults>
命令。
历史
- 2009 年 4 月 27 日:首次发布