Чи вважається прийнятним не викликати Dispose () на об'єкт TPL Task?


123

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

У .net 3.5 я зробив би це:

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

У .net 4 TPL є запропонованим способом. Загальна модель, яку я бачив, рекомендувала:

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

Однак StartNew()метод повертає Taskоб'єкт, який реалізується IDisposable. Це, мабуть, не помічають люди, які рекомендують цю схему. Документація MSDN щодо Task.Dispose()методу говорить:

"Завжди дзвоніть у розпорядження, перш ніж випустите останню посилання на завдання."

Ви не можете зателефонувати розпоряджатись завданням, поки воно не буде завершено, тому, якщо зачекати основний потік і розпочати виклик, в першу чергу переможеться точка виконання на фоновому потоці. Також, здається, не було жодної завершеної / завершеної події, яку можна було б використати для очищення.

Сторінка MSDN класу Task цього не коментує, а книга "Pro C # 2010 ..." рекомендує ту саму схему і не коментує видалення завдання.

Я знаю, якщо я просто лишаю його, фіналізатор зловить його врешті-решт, але це повернеться і кусає мене, коли я виконую багато завдань із вогню та забудьте, як це, і фіналізатор переповнюється?

Тому мої запитання:

  • Чи прийнятно не дзвонити Dispose()по Taskкласу в цьому випадку? І якщо так, то чому і чи існують ризики / наслідки?
  • Чи є документація, яка обговорює це?
  • Або є відповідний спосіб утилізації Taskпредмета, який я пропустив?
  • Або є інший спосіб виконувати завдання з вогнем та забуттям із TPL?

Відповіді:


108

Про це йде дискусія на форумах MSDN .

Стівен Туб, член команди Microsoft pfx, має це сказати:

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

Оновлення (жовтень 2012)
Стівен Туб опублікував блог під назвою Чи потрібно розпоряджатися завданнями? що дає детальніше та пояснює вдосконалення в .Net 4.5.

Підсумовуючи це: Вам не потрібно утилізувати Taskпредмети 99% часу.

Існує дві основні причини розпоряджатися об’єктом: звільнити некеровані ресурси своєчасно, детерміновано та уникнути витрат на запуск фіналізатора об'єкта. Жодне з них не стосується Taskбільшості випадків:

  1. Станом на .Net 4.5, єдиний раз , коли Taskвиділяє внутрішня ручка очікування (тільки некерований ресурс в Taskоб'єкті), коли ви явно використовувати IAsyncResult.AsyncWaitHandleз Task, і
  2. Сам Taskоб’єкт не має фіналізатора; сама ручка загорнута в об’єкт з фіналізатором, тому, якщо вона не виділена, фіналізатора не потрібно запускати.

3
Дякую, цікаво. Однак це суперечить документації MSDN. Чи є офіційне слово від MS або команди .net, що це прийнятний код. В кінці цієї дискусії також є питання, що "що робити, якщо впровадження зміниться в майбутній версії"
Simon P Stevens

Насправді я щойно помітив, що відповідач у цій нитці дійсно працює у Microsoft, мабуть, у команді pfx, тому я вважаю, що це офіційна відповідь. Але є думка, що внизу вона працює не у всіх випадках. Якщо можливий витік, я краще просто повернутися до ThreadPool.QueueUserWorkItem, який я знаю, що це безпечно?
Саймон П Стівенс

Так, дуже дивно, що є розпорядження, яке ви можете не зателефонувати. Якщо ви подивитеся на зразки тут, 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 ().

Клас Завдання додає до цього одну ручку, внутрішню подію вручну. Який найдешевший ресурс операційної системи є. Звичайно, його метод Dispose () може випустити лише одну ручку події, а не 5 ручок, які використовує Thread. Так, не турбуйся .

Не забудьте зацікавити вас властивістю IsFaulted завдання. Це досить потворна тема, детальніше про неї можна прочитати в цій статті бібліотеки MSDN . Якщо ви правильно впораєтеся з цим, ви також повинні мати у своєму коді гарне місце для розпорядження завданнями.


6
Але завдання не створює Threadв більшості випадків, воно використовує ThreadPool.
svick

-1

Я хотів би, щоб хтось зважив на техніку, показану в цьому дописі: Typesafe асинхронний виклик асинхронного делегата на C #

Схоже, простий метод розширення буде обробляти всі тривіальні випадки взаємодії із завданнями та матиме змогу викликати розпорядження ним.

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
Отож, такий код Codee ContinueWith насправді гірший за редудацію, оскільки це призведе до створення іншої задачі просто для того, щоб позбутися старої задачі. З тим, як це стоїть, в основному неможливо ввести блокування очікування в цей код коду окрім того, якби делегат дії, який ви передали йому, намагався маніпулювати самими Завданнями також правильно?
Кріс Марісіч

1
Можливо, ви зможете використовувати те, як лямбдаси захоплюють змінні трохи хитро, щоб подбати про друге завдання. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Гедеон Енгельберт

@GideonEngelberth, який, здавалося б, повинен працювати. Оскільки дисперс ніколи не повинен утилізуватися GC, він повинен залишатись дійсним до тих пір, поки лямбда не покликає себе розпоряджатися, припускаючи, що посилання все ще діє там / знизує плечима. Можливо, потрібна порожня спроба / ловити навколо цього?
Кріс Марісіч
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.