Рассмотрим следующий пример простейшего COM-объекта, который мы можем определить в C # (созданного с использованием Visual Studio 2010 SP1 с .NET Framework 4.0):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace CcwTestLib
{
[ComVisible(true)]
[Guid("8ABD40E2-05E2-4436-9EAD-073911357155")]
public class CcwTestObject
{
}
}
Мы компилируем эту сборку и регистрируем ее для COM-взаимодействия, используя regasm (или встроенную опцию в Visual Studio).
Теперь мы просто пишем неуправляемое консольное приложение Win32 на C ++, которое ничего не делает, кроме как создает экземпляр этого объекта и выпускает его 100 000 раз. Например, используя следующую программу:
#include "stdafx.h"
// {8ABD40E2-05E2-4436-9EAD-073911357155}
static const GUID CLSID_CcwTestObject =
{ 0x8abd40e2, 0x5e2, 0x4436, { 0x9e, 0xad, 0x7, 0x39, 0x11, 0x35, 0x71, 0x55 } };
int _tmain(int argc, _TCHAR* argv[])
{
CoInitializeEx(NULL, COINIT_MULTITHREADED);
IUnknown *pTestObject = NULL;
const int iCount = 100000;
wprintf(L"Allocating COM instance %i times...\n", iCount);
for (int i = 0; i < iCount; i++)
{
HRESULT hr = CoCreateInstance(CLSID_CcwTestObject,
NULL,
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(LPVOID*)&pTestObject);
if (FAILED(hr))
{
wprintf(L"Error: %i", hr);
return -1;
}
pTestObject->Release();
}
CoUninitialize();
return 0;
}
При запуске этого приложения в нашей локальной системе оно выполняется примерно за 820 мс и потребляет около 32 МБ памяти. Увеличение iCount
до 10 000 000 делает выполнение программы намного дольше (конечно), но, учитывая потребление памяти, оно увеличивается примерно до 92 МБ и остается там до конца выполнения программы. Пока ничего странного.
Теперь по интересной части, приводящей к моему вопросу. Давайте удалим атрибут Guid
из класса .NET (и отключим автоматическую регистрацию COM, если он включен, чтобы предыдущая регистрация все еще оставалась нетронутой в реестре) и перестроим сборку.
Мы снова запускаем тестовую программу с iCount
, установленным на 100 000. На этот раз программа завершается примерно за 90000 мс ! Это примерно в 100 раз медленнее , чем раньше!
Еще более интересно и неприятно, когда мы увеличиваем iCount
до 10 000 000 и запускаем программу. Если мы отслеживаем потребление памяти с помощью Process Explorer, VMMap или аналогичной программы, мы видим, что он медленно увеличивается, но не останавливается на 92 МБ, как мы могли бы ожидать. Вместо этого, кажется, продолжается вечно. Предположительно, приложение будет аварийно завершать работу при исчерпании пространства виртуальной памяти размером около 2 ГБ (поскольку это процесс x86), но, поскольку оно движется так медленно, мы не ожидали, что это произойдет в этом тесте, а остановились на 1200 МБ. *
Следует отметить, что использование COM-объекта, вызов его методов и т. Д. (Если мы их определили) работает нормально, как и должно, поскольку вся необходимая информация для создания объекта хранится в реестре. Часть этого в нашей системе выглядит следующим образом:
[HKEY_CLASSES_ROOT\CLSID\{8ABD40E2-05E2-4436-9EAD-073911357155}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="CcwTestLib.CcwTestObject"
"Assembly"="CcwTestLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"=file:///D:/Coding/Projects/CcwTest/CcwTestLib/bin/Debug/CcwTestLib.dll
Где CLSID правильно указывает на сборку и ее кодовую базу, а также на явный тип в сборке.
Мы также обнаружили, что изменение Guid в атрибуте на что-либо кроме того, с которым он зарегистрирован, создает ту же проблему.
Так почему это происходит? Это ошибка в .NET? И есть ли решение этой проблемы?
Я был бы очень рад получить некоторое представление об этой проблеме, которая заняла у нас около недели, чтобы сузить этот значительно упрощенный сценарий от обнаруженной утечки памяти в нашем продукте.