@bermanUnicorn

C# Как освобождать оперативную память завершенных Task'ов?

Добрый день.
Общая информация: Мной было написано приложение, в процессе работы которого создается достаточно много Task'ов. Каждый таск берет из некого множества входные параметры, создает новый экземпляр класса и запускается (класс состоит из череды методов, которые представляют из себя обработку данных. Никаких данных в приложение не возвращается, итог работы просто сохраняется на диск).

Проблема: Таски, которые уже гарантировано завершились с точки зрения логики моего приложения, т.е. сформировали и сохранили итог своей работы, продолжают занимать оперативную память до тех пор, пока не завершаться успехом или провалом вообще все множество запущенных тасков.
Например: В основном потоке(в цикле) я создаю и даю команду запуска 2 тысячам тасков. Информацию о них я заношу в коллекцию List и после постановки этих задач ожидаю их завершения через:
Task.WaitAll(taskList.ToArray());
Одновременно выполняется в среднем ~10 тасков (если судить по логу и temp файлам обработки) и по началу затрачивается около 700MB оперативной памяти, но постепенно это значение растёт до полной утилизации всего доступного RAM (до 10Gb за ~50 минут работы приложения). Объемов данных, которые бы могли действительно использовать столько оперативной памяти точно нет. Из-за этого я сделал вывод, что это скорее всего проблема в освобождении использованного RAM.

Вопрос: Как освобождать оперативную память завершенных Task'ов? Или, возможно, подсказать другое направления оптимизации, которое может мне помочь в данной ситуации. Может быть тащить за собой cancellation token или как то из тела таска делать dispose или etc.

P.S.:Профессионально не являюсь программистом и имею достаточно скромные знания. В конкретно взятом случае переписываю legacy поделку из корпоративной среды для получения знаний и опыта.
  • Вопрос задан
  • 3833 просмотра
Решения вопроса 1
@aaaleinik
в вашем случае большое потребление памяти (а возможно и других ресурсов) связано не с проблемами утилизации инстансов тасок.

В обычном сценарии, дефолтный TaskSheduler исполняет Task через ThreadPool.
При инициализации, ThreadPool создает себе небольшое количество рабочих потоков (начальное число равно количеству ядер в сисетме). Далее ThreadPool следит за их использованием, и если видит что текущее количество рабочих потоков не может обслужить все поступающиие задачи, то создает новый рабочий поток.
Формально, критерий выделения потока следующий: очередь поступивших задач не пуста И время которое прошло с момента взятия задачи из очери на исполнение больше 500мс И утилизация процессора меньше 80%.

Таким образом, когда вы ставите на выполнение 2000 Task, которые длительно выполняются, то ThreadPool начинает по медленно штамповать потоки. А вот они уже отнюдь не так легковесны как Task.
С архитектурной точки зрения, эта ситуация неправильная. Нельзя кидать 2000 задачь выполняться одновременно без какой-либо балансировки. Но утечки памяти здесь нет.

В вашем случае, можно написать кастомный TaskSheduler, который будет ограничивать степень параллелизма задач, которые отдаются в ThreadPool.
Или можно воспользоваться Parallel.ForEach, которому можно явно указать степень параллелизма.
Ответ написан
Пригласить эксперта
Ответы на вопрос 3
GavriKos
@GavriKos
В VisualStudio есть неплохой инструмент профилирования - можете посмотреть где течет память.
А вообще - пока есть переменная, ссылающаяся на экземпляр класса - память освобождена не будет. Из того что вы написали - вам нужно по завершении каждой таски удалять ее из taskList.
Ответ написан
@basrach
Сам Task, это класс с ~10 полями по 4 байта. Можно посчитать сколько займут в памяти 2000 штук. Очевидно проблема не в них. Как выше уже отметили, проблема не в самих тасках как таковых, а в том коде, который исполняется посредством тасков. И даже не в потоках. Попробуйте создать 2000 настоящих потоков (new System.Threading.Thread(...)) и выполнить там тривиальный код, навряд ли они смогут выжрать 10GB памяти.
Проблема в коде, который вы запускаете в этих тасках. Если вы не знакомы с автоматическими сброщиками мусора, то нужно про них почитать.
Любой код на C# - это плюс/минус метод. Создавая таск вы передаете ему ссылку на метод, который нужно выполнять, метод может быть именованным либо анонимным, неважно. Проблема в том, что по завершении этого метода, ресурсы, которые были использованы в этом методе не освобождаются. Неважно где исполняется метод: в таске, в потоке, просто так в основном потоке приложения. Вам нужно добиться того, чтобы при выходе из метода, которые вы передаете таску, все ресурсы использованные в этом методе были освобождены.
Если вы незнакомы со сборщиком мусора в CLR, то почитайте обязательно, да и вообще про сборку мусора.
В данном же ситуации, не видя кода, могу только посоветовать следующее. Далее для упрощения понимания будем считать, что у нас всего два участка кода: некий метод (это то что вы передаете в таск), и основной поток (вся остальная программа):
1) Если что-то создаете (вообще везде, а в методе особенно), любой класс, если у него есть метод .Close() или Dispose(), то обязательно вызывайте этот метод после того как класс вам больше не нужен.
2) Если есть возврат результата из метода, проверьте не возвращается ли сверх того, что вам нужно. Например, возвращается класс, с двумя полями, одно число, другое массив. Вам нужно из этого только число. Соответственно, поле с массивом нужно убрать из возвращаемого значения.
3) Упростите возвращаемый результат насколько это возможно. Например вам нужно подсчитать сумму элементов в N массивах. Вы запускаете N потоков и возвращаете N массивов, т.е. из каждого метода по массиву, а потом в основном потоке суммируете длины всех массивов. В этом случае как раз будет ощущение утечки памяти. Нужно возвращать сразу длину массива. И т.д.
4) Если есть добавление элементов из метода в коллекцию, которая объявлена в основном потоке. Проверить, очищается ли эта коллекция при выходе из метода. Или не добавляется и слишком много в эту коллекцию. Или Возможно в эту коллекцию добавляются слишком большие массивы и т.п.
5) Почти то же что и предыдущий пункт. Если есть какая-либо статическая коллекция или статические поля, переделайте на нестатические везде где возможно. А где невозможно, проследите чтобы в такую статическую коллекцию не добавлялись элементы из метода. Или если добавляются, то проверьте размер элементов, он должен быть минимальный.
6) Проследите, что не создаете больших массивов размером более 80кб. Если создаете, Измените на меньший размер если возможно. Например, если стоит задача подсчитать количество символов в файле, то не нужно читать его в память. Достаточно в цикле считывать по 8кб и суммировать результат.
7) Последнее. Перед выходом из метода вставьте:
System.Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce;
System.GC.Collect();
Ответ написан
Комментировать
@ofigenn
Создай список тасков. После завершения таска, таск должен удалить себя из списка. И да, почитай про MaxThreads. https://docs.microsoft.com/ru-ru/dotnet/api/system...
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы