строка ссылки [] утечка памяти при взаимодействии NET / COM

#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, даже если они выглядят как «настоящие» пользователи СЕТИ!