Почему использование StreamSocket в цикле вызывает утечку памяти?

Я разрабатываю решение С#, UWP 10, которое общается с сетевым устройством, используя быстрый, непрерывный цикл чтения/записи. StreamSocket, предлагаемый API, по-видимому, отлично работает, пока я не понял, что произошла утечка памяти: в куче есть накопление Task<uint32> в порядке сотен в минуту.

Я использую простой старинный цикл while (true) внутри async Task или используя самостоятельную проводку ActionBlock<T> с потоком данных TPL (в соответствии с этим ответом), результат тот же.

Я могу еще больше изолировать проблему, если я удалю чтение из сокета и сосредоточусь на написании: Использую ли я подход DataWriter.StoreAsync или более прямой StreamSocket.OutputStream.WriteAsync(IBuffer buffer), проблема остается. Кроме того, добавление к нему .AsTask() не имеет значения.

Даже когда сборщик мусора работает, эти Task<uint32> никогда не удаляются из кучи. Все эти задачи полны (RanToCompletion), не имеют ошибок или какого-либо другого значения свойства, которое указывает на "не совсем готовый к возврату".

Кажется, у меня есть проблема с эта страница (массив байтов, идущий из управляемого в неуправляемый мир, предотвращает выпуск памяти), но предписанное решение кажется довольно резким: единственный способ обойти это - написать всю логику связи в С++/CX. Надеюсь, это неправда; конечно, другие разработчики С# успешно реализовали непрерывные высокоскоростные сетевые коммуникации без утечек памяти. И, конечно же, Microsoft не выпустит API, который работает без утечек памяти в С++/CX

ИЗМЕНИТЬ

В соответствии с запросом, некоторый пример кода. В моем собственном коде слишком много слоев, но гораздо более простой пример можно наблюдать с этот образец Microsoft. Я сделал простую модификацию, чтобы отправить 1000 раз в цикле, чтобы выделить проблему. Это соответствующий код:

public sealed partial class Scenario3 : Page
{
    // some code omitted

    private async void SendHello_Click(object sender, RoutedEventArgs e)
    {
        // some code omitted

        StreamSocket socket = //get global object; socket is already connected

        DataWriter writer = new DataWriter(socket.OutputStream);

        for (int i = 0; i < 1000; i++)
        {
            string stringToSend = "Hello";
            writer.WriteUInt32(writer.MeasureString(stringToSend));
            writer.WriteString(stringToSend);
            await writer.StoreAsync();
        }
    }
}

При запуске приложения и подключении сокета в куче есть только экземпляр Task<uint32>. После нажатия кнопки "SendHello" имеется 86 экземпляров. После нажатия 2 раза: 129 экземпляров.

Изменить # 2 После запуска моего приложения (с плотным циклом отправки/получения) в течение 3 часов я вижу, что определенная проблема: 0,5 миллиона экземпляров Task, которые никогда не получат GC'd, а память приложений увеличилась с начального 46 МБ до 105 МБ. Очевидно, что это приложение не может работать неопределенно. Однако... это относится только к запуску в режиме отладки. Если я скомпилирую свое приложение в режиме выпуска, разверните его и запустите, проблем с памятью не будет. Я могу оставить его работать всю ночь, и ясно, что память управляется должным образом. Дело закрыто.

Ответ 1

имеется 86 экземпляров. После нажатия 2 раза: 129 экземпляров.

Это совершенно нормально. И сильный намек на то, что настоящая проблема заключается в том, что вы не знаете, как правильно интерпретировать отчет профайлера памяти.

Задача звучит как очень дорогой объект, у нее много шума для доллара, а поток - самого дорогостоящего объекта операционной системы, который вы когда-либо создавали. Но это не так, объект Task на самом деле является мелким объектом. Он занимает всего 44 байта в 32-битном режиме, 80 байтов в 64-битном режиме. Поистине дорогостоящий ресурс не принадлежит Task, менеджер threadpool заботится об этом.

Это означает, что вы можете создать объекты лота объектов Task, прежде чем вы наложите достаточное давление на кучу GC для запуска коллекции. Около 47 тысяч из них заполняют сегмент # 0 в 32-битном режиме. Еще много на сервере, сотни тысяч, его сегменты намного больше.

В вашем фрагменте кода объекты Task являются единственными объектами, которые вы на самом деле создаете. Таким образом, цикл for for (;;) не настолько близок к циклу, чтобы когда-либо видеть количество объектов задачи, уменьшающихся или ограничивающих.

Итак, это обычная история, обвинения .NET Framework, имеющие утечки, особенно в отношении таких типов основных объектов, которые используются в серверных приложениях, которые работают в течение нескольких месяцев, навсегда сильно преувеличены. Двойное угадывание сборщика мусора всегда сложно, вы обычно получаете только уверенность в том, что приложение работает в течение нескольких месяцев и никогда не прерывается в OOM.

Ответ 2

Я бы создал и закрол DataWriter внутри for.