65.9K
CodeProject 正在变化。 阅读更多。
Home

ATL OLEDB 内存泄漏 2:CDynamicParameterAccessor

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2009年4月27日

CPOL

3分钟阅读

viewsIcon

21613

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 日:首次发布
© . All rights reserved.