#c# #.net #memory-leaks #interop
#c# #.net #утечки памяти #взаимодействие
Вопрос:
Недавно я обнаружил очень странную (для меня) утечку памяти для COM-объекта IEnumString, используемого из C #. В частности, вызов IEnumString.Следующий метод, использующий массив строк, который уже содержал значения, полученные в результате предыдущего вызова, вызвал утечку памяти.
IEnumString выглядела следующим образом на стороне C #:
[InterfaceType(1)]
[Guid("00000101-0000-0000-C000-000000000046")]
public interface IEnumString
{
void Clone(out IEnumString ppenum);
void RemoteNext(int celt, string[] rgelt, out int pceltFetched);
void Reset();
void Skip(int celt);
}
Подобный вызов метода RemoteNext (Next) вызвал утечку, которая была проверена путем многократного запуска в течение длительного времени и бесконечного увеличения счетчика «Частных байтов».
string[] item = new string[100]; // OBS! Will be re-used for each call!
for (; ; )
{
int fetched;
enumString.RemoteNext(item.Length, item, out fetched);
if (fetched > 0)
{
for (int i = 0; i < fetched; i)
{
// do something with item[i]
}
}
else
{
break;
}
}
Но создание нового массива string item[] для каждого вызова каким-то образом привело к исчезновению утечки.
for (; ; )
{
int fetched;
string[] item = new string[100]; // Create a new instance for each call.
enumString.RemoteNext(item.Length, item, out fetched);
if (fetched > 0)
{
for (int i = 0; i < fetched; i)
{
// do something with item[i]
}
}
else
{
break;
}
}
Что именно пошло не так в первом случае? Я предполагаю, что происходит то, что память COM, выделенная для аргумента rgelt IEnumString.Next, которая должна быть освобождена вызывающей стороной, почему-то не является.
Но второй случай, как ни странно, работает.
Редактировать: Для получения некоторой дополнительной информации, вот как выглядит «реализация» метода RemoteNext в ILDASM и .NET Reflector.
.method public hidebysig newslot abstract virtual
instance void RemoteNext([in] int32 celt,
[in][out] string[] marshal( lpwstr[ 0]) rgelt,
[out] int32amp; pceltFetched) runtime managed internalcall
{
} // end of method IEnumString::RemoteNext
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime)]
void RemoteNext(
[In] int celt,
[In, Out, MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPWStr, SizeParamIndex=0)] string[] rgelt,
out int pceltFetched);
Правка 2: Вы также можете сделать так, чтобы утечка отображалась во втором случае отсутствия утечки, просто добавив строковое значение в массив string (содержащий только значения null) перед вызовом RemoteNext.
string[] item = new string[100]; // Create a new instance for each call.
item[0] = "some string value"; // THIS WILL CAUSE A LEAK
enumString.RemoteNext(item.Length, item, out fetched);
Таким образом, кажется, что массив элементов должен быть пустым, чтобы уровень маршалинга правильно освобождал скопированные в него неуправляемые строки. Даже в этом случае массив вернет те же значения, т. Е. наличие непустого массива не приводит к возврату неправильных строковых значений, это просто утечка некоторых.
Комментарии:
1. Следите за синтетическими тестами, которые запускают один и тот же фрагмент кода миллионы раз. Использовать Perfmon.exe и обратите внимание на количество сборок мусора для вашего тестового процесса. COM использует неуправляемую память, это не оказывает большого давления на GC.
2. Это не синтетический тест, я запустил не только отображаемый фрагмент кода, но и весь программный сегмент, в котором он использовался. (Это небольшое тестовое приложение, которое извлекает пространство имен (ветви и конечные элементы) серверного приложения и просто распечатывает их. Это делается снова и снова. Если оставить запуск с первым фрагментом кода, процесс довольно скоро будет потреблять сотни МБ памяти, в то время как со вторым он стабилизируется примерно на 20 МБ).
3. Я также проверил использование памяти кучи GC с помощью Process Explorer, в любом случае она не потребляла много памяти, поэтому я почти уверен, что утечка произошла из-за неуправляемой COM-памяти.
Ответ №1:
IEnumString
— это всего лишь интерфейс. Что является базовым COM-объектом? Это главный подозреваемый.
Посмотрите на неуправляемое объявление IEnumString
:
HRESULT Next(
[in] ULONG celt,
[out] LPOLESTR *rgelt,
[out] ULONG *pceltFetched
);
Как вы видите, второй параметр rgelt
— это просто указатель на массив строк — ничего особенного, но когда вы выполняете управляемый вызов
string[] item = new string[100]; // Create a new instance for each call.
item[0] = "some string value"; // THIS WILL CAUSE A LEAK
enumString.RemoteNext(item.Length, item, out fetched);
кажется, ваша строка в item[0]
преобразована в LPOLESTR, который не освобожден должным образом. Поэтому попробуйте это:
string[] item = new string[1];
for (; ; )
{
int fetched;
item[0] = null;
enumString.RemoteNext(1, item, out fetched);
if (fetched == 1)
{
// do something with item[0]
}
else
{
break;
}
}
Комментарии:
1. Да, вы, конечно, правы! Я не учел, что уровень сортировки преобразует управляемые строки, уже существующие в массиве item (rgelt), в неуправляемую COM-память, которую необходимо освободить. Ошибка не в объекте реализации IEnumString, хотя параметр rgelt четко определен как параметр [out], поэтому для него не нужно освобождать какую-либо память. Просто эта «исходящая информация» потеряна на . СЕТЕВАЯ сторона, поэтому нет предупреждения или ошибки, когда вызывающий выполняет неправильные действия. Думаю, лучше всегда использовать оболочки для взаимодействия COM, даже если они выглядят как «настоящие» пользователи СЕТИ!