Задачи TPL + динамические == OutOfMemoryException?

Я работаю над потоковым клиентом Twitter - после 1-2 дней постоянной работы я получаю использование памяти> 1,4 гигабайта (32-разрядный процесс), и вскоре после того, как он достигнет этого количества, я получу нехватку памяти. исключение для кода, который по сути такой (этот код выдаст ошибку через ‹30 секунд на моей машине):

while (true)
{
  Task.Factory.StartNew(() =>
  {
    dynamic dyn2 = new ExpandoObject();

    //get a ton of text, make the string random 
    //enough to be be interned, for the most part
    dyn2.text = Get500kOfText() + Get500kOfText() + DateTime.Now.ToString() + 
      DateTime.Now.Millisecond.ToString(); 
  });
}

Я профилировал это, и это определенно связано с тем, что класс находится в DLR (насколько я помню, у меня нет подробной информации здесь) xxRuntimeBinderxx и xxAggregatexx.

Этот ответ Эрика Липперта (Microsoft), кажется, указывает на то, что я m создание выражений, анализирующих объекты за кулисами, которые никогда не проходят сборщик мусора, даже если в моем коде нет ссылок на что-либо.

Если это так, есть ли в приведенном выше коде способ предотвратить или уменьшить его?

Мой запасной вариант - исключить динамическое использование, но я бы предпочел этого не делать.

Спасибо

Обновление:

14.12.12:

ОТВЕТ:

Способ заставить этот конкретный пример освободить свои задачи состоял в том, чтобы yield (Thread.Sleep(0)), который затем позволял GC обрабатывать освободившиеся задачи. Я предполагаю, что цикл сообщения/события не может быть обработан в этом конкретном случае.

В реальном коде, который я использовал (поток данных TPL), я не вызывал Complete() для блоков, потому что они должны были быть бесконечный поток данных - задача будет принимать сообщения Twitter до тех пор, пока Twitter их отправляет. В этой модели никогда не было причин сообщать каким-либо блокам, что они были выполнены, потому что они никогда не БЫЛИ выполнены, пока приложение работало.

К сожалению, не похоже, что блоки потока данных никогда не разрабатывались для очень долгой работы или обработки бесчисленного количества элементов, потому что они фактически хранят ссылку на все, что в них отправляется. Если я ошибаюсь, пожалуйста, дайте мне знать.

Таким образом, обходной путь заключается в том, чтобы периодически (в зависимости от использования вашей памяти - у меня было каждые 100 000 сообщений в Твиттере) освобождать блоки и настраивать их снова.

В соответствии с этой схемой потребление моей памяти никогда не превышает 80 мегабайт, а после повторного использования блоков и форсирования сборки мусора куча gen2 возвращается к 6 мегабайтам, и все снова в порядке.

17.10.12:

  • "Это не делает ничего полезного": этот пример просто позволяет вам быстро сгенерировать проблему. Он состоит из нескольких сотен строк кода, которые не имеют никакого отношения к проблеме.
  • «Бесконечный цикл, создающий задачу и, в свою очередь, создающий объекты»: помните — это просто быстро демонстрирует проблему — фактический код находится там, ожидая дополнительных потоковых данных. Кроме того, глядя на код, все объекты создаются внутри лямбды Action‹> в задаче. Почему это не очищается (в конце концов) после того, как оно выходит за рамки? Проблема также не из-за того, что это делается слишком быстро - фактическому коду требуется больше дня, чтобы достичь исключения нехватки памяти - это просто делает его достаточно быстрым, чтобы попробовать что-то.
  • "Гарантировано ли освобождение задач?" Объект есть объект, не так ли? Насколько я понимаю, планировщик просто использует потоки в пуле, и лямбда-выражение, которое он выполняет, будет выброшено после того, как оно будет запущено, несмотря ни на что.

person dethSwatch    schedule 17.10.2012    source источник
comment
Я не вижу в этом ничего продуктивного; кажется, что он предназначен только для того, чтобы сломать систему. Учитывая это; почему вы удивляетесь, что это ломает систему. Если у вас есть настоящая задача, которую нужно выполнить, почему бы не рассказать нам, что это за задача? Например, прагматичным решением было бы ограничить количество создаваемых задач, чтобы не потреблять слишком много памяти.   -  person Servy    schedule 17.10.2012
comment
Я не уверен, чего вы ожидали. Бесконечный цикл создает задачу и, в свою очередь, создает объекты. Объекты потребляют память...   -  person Lews Therin    schedule 17.10.2012
comment
@LewsTherin Ну, теоретически задачи могут завершиться и выйти за рамки, тем самым освободив память для новых задач. На практике это, вероятно, происходит, но происходит медленнее, чем потребляется новая память.   -  person Servy    schedule 17.10.2012
comment
@Servy Гарантировано ли освобождение задач? Независимо от ГК?   -  person Lews Therin    schedule 17.10.2012
comment
@LewsTherin Не зависимо от GC. В какой-то момент этот метод явно завершается (задача не зацикливается навсегда). Когда этот метод завершает работу, делегат, в котором выполняется задача, выходит из области видимости, поэтому все переменные, локальные для этого метода, имеют право на сборку мусора (за исключением таких вещей, как закрытые переменные или другие поднятые переменные). Что касается самой задачи, она не будет потреблять огромное количество памяти, но я полагаю, что она становится доступной для сборки мусора вскоре после завершения выполнения делегата, учитывая, что она не хранится ни в одной переменной.   -  person Servy    schedule 17.10.2012
comment
Прямо спасибо за урок :)   -  person Lews Therin    schedule 17.10.2012
comment
Бесконечный цикл создает задачу и, в свою очередь, создает объекты: ваш контраргумент не верен: вы можете генерировать (ставить в очередь) задачу быстрее, чем они могут быть обработаны. Это, конечно, приводит к утечке памяти, потому что освобождение происходит медленнее, чем выделение. Почему вы не можете воспроизвести это с помощью однопоточного цикла или постоянного количества потоков?   -  person usr    schedule 17.10.2012
comment
Я поместил засыпание в этот конкретный код в диапазоне от полсекунды до целой секунды, и, похоже, это не имеет значения (вы пробовали код?) - также имейте в виду, что для этого требуется больше дня. происходит в производственном коде, где потоки не генерируются как можно быстрее, как это делает этот код, чтобы быстро показать ошибку   -  person dethSwatch    schedule 17.10.2012
comment
Я запустил этот код, размер фиксации процесса сильно варьировался, достигая 1,5 ГБ. Но никогда не взрывается. Диагностика заключается в том, что запуск множества потоков, использующих много памяти, порождает процесс, использующий много памяти. Если у вас достаточно ядер, это, безусловно, может привести к OOM. 64-разрядная версия Windows — дешевое и простое решение.   -  person Hans Passant    schedule 17.10.2012
comment
Я думаю, что есть что-то в том, что говорит ОП. Он не создает бесконечных задач в продакшене, но у него есть проблема. Я просто думаю, что ему нужно предоставить репродукцию, которая не имеет очевидной ошибки, отличной от той, которую он хочет показать.   -  person usr    schedule 17.10.2012


Ответы (2)


Это больше связано с тем, что производитель намного опережает потребителя, чем с DLR. Цикл создает задачи как можно быстрее — и задачи не запускаются «немедленно». Легко понять, насколько он может отставать:

        int count = 0;

        new Timer(_ => Console.WriteLine(count), 0, 0, 500);

        while (true)
        {
            Interlocked.Increment(ref count);

            Task.Factory.StartNew(() =>
            {
                dynamic dyn2 = new ExpandoObject();
                dyn2.text = Get500kOfText() + Get500kOfText() + DateTime.Now.ToString() +
                  DateTime.Now.Millisecond.ToString();

                Interlocked.Decrement(ref count);
            });
        }

Вывод:

324080
751802
1074713
1620403
1997559
2431238

Это много для трехсекундного планирования. Удаление Task.Factory.StartNew (однопоточное выполнение) дает стабильную память.

Однако репродукция, которую вы дали, кажется немного надуманной. Если слишком много одновременных задач действительно является вашей проблемой, вы можете попробовать настраиваемый планировщик задач, который ограничивает одновременное планирование.

person Asti    schedule 17.10.2012
comment
правильно, но помните - производственный код работает со скоростью твиттера, которая при моем использовании составляет максимум 10 сообщений в секунду, за которыми следует несколько секунд бездействия. Кроме того, размещение стратегических снов в этом примере по-прежнему будет генерировать исключение нехватки памяти, просто это займет больше времени. - person dethSwatch; 17.10.2012

Проблема здесь не в том, что задачи, которые вы создаете, не очищаются. Asti продемонстрировал, что ваш код создает задачи быстрее, чем они могут быть обработаны, поэтому, пока вы очищаете память о выполненных задачи, вы все равно закончите в конце концов.

Вы сказали:

добавление стратегического сна в этот пример все равно вызовет исключение нехватки памяти, просто это займет больше времени.

Вы не показали код для этого или любой другой пример, который ограничивает количество одновременных задач. Я предполагаю, что вы в какой-то степени ограничиваете создание, но скорость создания все же выше, чем скорость потребления. Вот мой собственный ограниченный пример:

int numConcurrentActions = 100000;
BlockingCollection<Task> tasks = new BlockingCollection<Task>();

Action someAction = () =>
{
    dynamic dyn = new System.Dynamic.ExpandoObject();

    dyn.text = Get500kOfText() + Get500kOfText() 
        + DateTime.Now.ToString() + DateTime.Now.Millisecond.ToString();
};

//add a fixed number of tasks
for (int i = 0; i < numConcurrentActions; i++)
{
    tasks.Add(new Task(someAction));
}

//take a task out, set a continuation to add a new one when it finishes, 
//and then start the task.
foreach (Task t in tasks.GetConsumingEnumerable())
{
    t.ContinueWith(_ =>
    {
        tasks.Add(new Task(someAction));
    });
    t.Start();
}

Этот код гарантирует, что одновременно будет выполняться не более 100 000 задач. Когда я запускаю это, память стабильна (при усреднении за несколько секунд). Он ограничивает задачи, создавая фиксированное число, а затем устанавливая продолжение для планирования новой задачи всякий раз, когда завершается существующая.

Это говорит нам о том, что, поскольку ваши реальные данные основаны на фиде из какого-то внешнего источника, вы получаете данные из этого фида немного быстрее, чем можете их обработать. Здесь у вас есть несколько вариантов. Вы можете ставить элементы в очередь по мере их поступления, гарантировать, что в настоящее время может работать только ограниченное количество, и отбрасывать запросы, если вы превысили свои возможности (или найти какой-либо другой способ фильтрации ввода, чтобы вы не обрабатывали все это) , или вы можете просто приобрести лучшее оборудование (или оптимизировать имеющийся у вас метод обработки), чтобы вы могли обрабатывать запросы быстрее, чем они могут быть созданы.

Хотя обычно я бы сказал, что люди склонны пытаться оптимизировать код, когда он уже работает «достаточно быстро», это явно не ваш случай. У вас есть довольно жесткий контрольный показатель, который вам нужно достичь; вам нужно обрабатывать элементы быстрее, чем они приходят. В настоящее время вы не соответствуете этому критерию (но, поскольку он работает некоторое время, прежде чем дать сбой, вы не должны быть этими далеко).

person Servy    schedule 17.10.2012
comment
Я проверю вашу гипотезу о том, что он действительно производит больше, чем может быть потреблен - я действительно ставлю каждое сообщение в очередь (я использую пакетный блок потока данных TPL из 10), прежде чем воздействовать на них, и аппаратное обеспечение - это Core i7 4 ядра + 12 ГБ, поэтому это не проблема. -- Но меня больше всего беспокоит то, что часто бывает несколько секунд бездействия (никто не пишет в Твиттере), и довольно мало шансов (я думаю), что он не сможет наверстать упущенное в эти периоды. - person dethSwatch; 18.10.2012