Считается ли приемлемым не вызывать Dispose () для объекта задачи TPL?

123

Я хочу запустить задачу в фоновом потоке. Не хочу ждать завершения задач.

В .net 3.5 я бы сделал это:

ThreadPool.QueueUserWorkItem(d => { DoSomething(); });

В .net 4 рекомендуется использовать TPL. Я видел рекомендуемый общий шаблон:

Task.Factory.StartNew(() => { DoSomething(); });

Однако StartNew()метод возвращает Taskобъект, который реализует IDisposable. Похоже, что люди, рекомендующие этот шаблон, не замечают этого. В документации MSDN по этому Task.Dispose()методу говорится:

«Всегда вызывайте Dispose перед тем, как отпустить последнюю ссылку на Задачу».

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

На странице MSDN в классе Task это не комментируется, а книга «Pro C # 2010 ...» рекомендует тот же шаблон и не дает никаких комментариев по удалению задач.

Я знаю, что если я просто оставлю это, финализатор поймает его в конце, но вернется ли он и укусит меня, когда я выполняю много задач типа «огонь и забывай», и поток финализатора будет перегружен?

Итак, мои вопросы:

  • Приемлемо ли не звонить Dispose()по Taskклассу в этом случае? И если да, то почему и существуют ли риски / последствия?
  • Есть ли документация, в которой это обсуждается?
  • Или есть подходящий способ избавиться от Taskпредмета, который я пропустил?
  • Или есть другой способ выполнить задачи «выстрелил и забыл» с TPL?
Саймон П. Стивенс
источник
1
По теме: Правильный путь к огню и забытию (см. Ответ )
Саймон П. Стивенс

Ответы:

108

Об этом обсуждают на форумах MSDN .

Стивен Туб, член команды Microsoft pfx, сказал следующее:

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

Обновление (октябрь 2012 г.)
Стивен Туб опубликовал блог под названием « Нужно ли мне избавляться от задач?» который дает более подробную информацию и объясняет улучшения в .Net 4.5.

Подводя итог: вам не нужно избавляться от Taskпредметов в 99% случаев.

Есть две основные причины для удаления объекта: своевременное и детерминированное освобождение неуправляемых ресурсов и избежание затрат на запуск финализатора объекта. Ни то, ни другое не применимо в Taskбольшинстве случаев:

  1. По состоянию на .Net 4.5, единственный раз , когда Taskвыделяет внутренняя ручка ожидания (только неуправляемый ресурс в Taskобъекте), когда вы явно использовать IAsyncResult.AsyncWaitHandleиз Task, и
  2. Сам Taskобъект не имеет финализатора; дескриптор сам заключен в объект с финализатором, поэтому, если он не выделен, финализатор не запускается.
Кирилл Музыков
источник
3
Спасибо, интересно. Однако это противоречит документации MSDN. Есть ли официальное сообщение от MS или команды .net о том, что это приемлемый код. В конце обсуждения также поднимается вопрос, что «что, если реализация изменится в будущей версии»
Саймон П. Стивенс
На самом деле, я только что заметил, что ответчик в этом потоке действительно работает в Microsoft, по-видимому, в команде pfx, поэтому я полагаю, что это своего рода официальный ответ. Но есть предположение, что это работает не во всех случаях. Если есть потенциальная утечка, мне лучше просто вернуться к ThreadPool.QueueUserWorkItem, который, как я знаю, безопасен?
Саймон П. Стивенс
Да, очень странно, что есть Dispose, который можно не вызывать. Если вы посмотрите на образцы здесь msdn.microsoft.com/en-us/library/dd537610.aspx и здесь msdn.microsoft.com/en-us/library/dd537609.aspx, они не удаляют задачи. Однако образцы кода в MSDN иногда демонстрируют очень плохие методы. Также парень ответил на вопрос, работает ли в Microsoft.
Кирилл Музыков
2
@Simon: (1) Документ MSDN, который вы цитируете, является общим советом, в конкретных случаях есть более конкретные рекомендации (например, не нужно использовать EndInvokeв WinForms при использовании BeginInvokeдля запуска кода в потоке пользовательского интерфейса). (2) Стивен Тауб хорошо известен как постоянный докладчик по эффективному использованию PFX (например, на channel9.msdn.com ), так что если кто-то может дать хорошее руководство, то он его. Обратите внимание на его второй абзац: временами лучше оставить дело финализатору.
Ричард
12

Это та же проблема, что и с классом Thread. Он использует 5 дескрипторов операционной системы, но не реализует IDisposable. Хорошее решение исходных дизайнеров, конечно, есть несколько разумных способов вызвать метод Dispose (). Сначала вам нужно вызвать Join ().

Класс Task добавляет к этому один дескриптор - внутреннее событие ручного сброса. Какой ресурс операционной системы самый дешевый. Конечно, его метод Dispose () может освободить только этот один дескриптор события, а не 5 дескрипторов, которые использует Thread. Да, не беспокойтесь .

Помните, что вас должно интересовать свойство IsFaaled задачи. Это довольно неприятная тема, вы можете прочитать о ней в этой статье библиотеки MSDN . Как только вы справитесь с этим должным образом, у вас также должно быть хорошее место в вашем коде для размещения задач.

Ганс Пассан
источник
6
Но Threadв большинстве случаев задача не создает объект , а использует ThreadPool.
svick
-1

Мне бы очень хотелось, чтобы кто-то оценил технику, показанную в этом посте: Типичный вызов асинхронного делегата типа «выстрелил и забыл» в C #

Похоже, что простой метод расширения будет обрабатывать все тривиальные случаи взаимодействия с задачами и сможет вызывать для него dispose.

public static void FireAndForget<T>(this Action<T> act,T arg1)
{
    var tsk = Task.Factory.StartNew( ()=> act(arg1),
                                     TaskCreationOptions.LongRunning);
    tsk.ContinueWith(cnt => cnt.Dispose());
}
Крис Маришич
источник
3
Конечно, это не может избавиться от Taskэкземпляра, возвращенного ContinueWith, но, как видите, цитата из Стивена Туба является принятым ответом: нечего удалять, если ничто не выполняет блокирующее ожидание для задачи.
Ричард
1
Как упоминает Ричард, ContinueWith (...) также возвращает второй объект Task, который затем не удаляется.
Саймон П. Стивенс
1
Таким образом, в этом случае код ContinueWith на самом деле хуже, чем избыточный, поскольку он приведет к созданию другой задачи, просто чтобы избавиться от старой задачи. При таком положении дел в принципе было бы невозможно ввести блокирующее ожидание в этот блок кода, кроме того, если делегат действия, который вы ему передали, также пытался управлять самими задачами?
Крис Марисич
1
Вы можете использовать то, как лямбда-выражения захватывают переменные, немного хитрым способом, чтобы решить вторую задачу. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Гидеон Энгельберт
@GideonEngelberth, который, казалось бы, должен работать. Поскольку сборщик мусора никогда не должен удалять утилиту disper, она должна оставаться действительной до тех пор, пока лямбда не вызовет себя для удаления, при условии, что ссылка все еще действительна / пожимает плечами. Может быть, нужна пустая попытка / уловка?
Крис Марисич