Czy dopuszczalne jest niewywoływanie metody Dispose () na obiekcie zadania TPL?


123

Chcę uruchomić zadanie w wątku w tle. Nie chcę czekać na zakończenie zadań.

W .net 3.5 zrobiłbym to:

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

W .net 4 TPL jest sugerowanym sposobem. Typowy zalecany wzór, który widziałem, to:

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

Jednak StartNew()metoda zwraca Taskobiekt, który implementuje IDisposable. Wydaje się, że jest to przeoczane przez osoby, które zalecają ten wzór. Dokumentacja MSDN dotycząca Task.Dispose()metody mówi:

„Zawsze wywołuj Dispose, zanim zwolnisz ostatnie odniesienie do zadania”.

Nie możesz wywołać dispose na zadaniu, dopóki nie zostanie ono zakończone, więc pozostawienie głównego wątku oczekiwania i wywołania dispose pokonałoby w pierwszej kolejności punkt robienia w wątku w tle. Wydaje się również, że nie ma żadnego zakończonego / zakończonego wydarzenia, które można by wykorzystać do czyszczenia.

Strona MSDN w klasie Task nie komentuje tego, a książka „Pro C # 2010 ...” zaleca ten sam wzorzec i nie komentuje usuwania zadań.

Wiem, że jeśli po prostu zostawię to, finalizator w końcu to złapie, ale czy to wróci i mnie ugryzie, gdy będę wykonywać wiele zadań typu „odpal i zapomnij”, a wątek finalizatora zostanie przeciążony?

Więc moje pytania to:

  • Czy dopuszczalne jest, aby nie dzwonić Dispose()na Taskklasy w tym przypadku? A jeśli tak, dlaczego i czy istnieje ryzyko / konsekwencje?
  • Czy jest jakaś dokumentacja, która to omawia?
  • A może istnieje odpowiedni sposób na pozbycie się Taskprzedmiotu, który przegapiłem?
  • A może istnieje inny sposób wykonywania zadań „odpal i zapomnij” z TPL?

Odpowiedzi:


108

Jest dyskusja na ten temat na forach MSDN .

Stephen Toub, członek zespołu Microsoft pfx, ma do powiedzenia:

Task.Dispose istnieje z powodu tego, że Task potencjalnie zawija uchwyt zdarzenia używany podczas oczekiwania na zakończenie zadania, w przypadku gdy oczekujący wątek faktycznie musi zostać zablokowany (w przeciwieństwie do obracania lub potencjalnego wykonywania zadania, na które czeka). Jeśli wszystko, co robisz, to używanie kontynuacji, ten uchwyt zdarzenia nigdy nie zostanie przydzielony
...
prawdopodobnie lepiej polegać na finalizacji, aby zająć się sprawami.

Aktualizacja (październik 2012)
Stephen Toub opublikował blog zatytułowany Czy muszę pozbyć się zadań? co zawiera więcej szczegółów i wyjaśnia ulepszenia w .Net 4.5.

Podsumowując: nie musisz pozbywać się Taskprzedmiotów w 99% przypadków.

Istnieją dwa główne powody, dla których należy pozbyć się obiektu: w celu zwolnienia niezarządzanych zasobów w wyznaczonym czasie i w celu uniknięcia kosztów uruchamiania finalizatora obiektu. Żadne z nich nie ma zastosowania przez Taskwiększość czasu:

  1. Począwszy od .NET 4.5, jedynym razem Taskprzydziela wewnętrzny uchwyt wait (jedyny niekontrolowana zasobów w Taskobiekcie) jest wtedy, gdy wyraźnie używać IAsyncResult.AsyncWaitHandlez Taski
  2. Sam Taskobiekt nie ma finalizatora; sam uchwyt jest opakowany w obiekt z finalizatorem, więc jeśli nie zostanie przydzielony, nie ma finalizatora do uruchomienia.

3
Dzięki, ciekawe. Jest to jednak sprzeczne z dokumentacją MSDN. Czy jest jakieś oficjalne słowo od MS lub zespołu .net, że jest to dopuszczalny kod? Jest także punktem podniesione pod koniec tej dyskusji, że „co jeśli realizacja zmian w wersji przyszłości”
Simon P Stevens

Właściwie właśnie zauważyłem, że osoba odpowiadająca w tym wątku rzeczywiście działa w firmie Microsoft, pozornie w zespole pfx, więc przypuszczam, że jest to swego rodzaju oficjalna odpowiedź. Ale na dole jest sugestia, że ​​nie działa we wszystkich przypadkach. Jeśli istnieje potencjalny wyciek, czy lepiej po prostu wrócić do ThreadPool.QueueUserWorkItem, o którym wiem, że jest bezpieczny?
Simon P Stevens

Tak, to bardzo dziwne, że istnieje Dyspozycja, której możesz nie wywołać. Jeśli spojrzysz na przykłady tutaj msdn.microsoft.com/en-us/library/dd537610.aspx, a tutaj msdn.microsoft.com/en-us/library/dd537609.aspx, nie usuwają one zadań. Jednak przykłady kodu w MSDN czasami pokazują bardzo złe techniki. Również facet, który odpowiedział na pytanie, pracuje dla Microsoftu.
Kirill Muzykov

2
@Simon: (1) Cytowany przez Ciebie dokument MSDN to ogólna rada, konkretne przypadki zawierają bardziej szczegółowe porady (np. Nie ma potrzeby używania go EndInvokew WinForms podczas używania BeginInvokedo uruchamiania kodu w wątku interfejsu użytkownika). (2) Stephen Toub jest dość dobrze znany jako regularny mówca na temat efektywnego wykorzystania PFX (np. Na channel9.msdn.com ), więc jeśli ktoś może udzielić dobrych wskazówek, to on jest tym. Zwróć uwagę na jego drugi akapit: są chwile, gdy pozostawiasz sprawy finalizatorowi, a jest lepiej.
Richard,

12

Jest to ten sam problem, co w przypadku klasy Thread. Zużywa 5 uchwytów systemu operacyjnego, ale nie implementuje IDisposable. Dobra decyzja pierwotnych projektantów, oczywiście istnieje kilka rozsądnych sposobów wywołania metody Dispose (). Najpierw musisz zadzwonić do Join ().

Klasa Task dodaje do tego jeden uchwyt, wewnętrzne zdarzenie resetowania ręcznego. Jaki jest najtańszy dostępny zasób systemu operacyjnego. Oczywiście jego metoda Dispose () może zwolnić tylko jeden uchwyt zdarzenia, a nie 5 uchwytów używanych przez Thread. Tak, nie przejmuj się .

Uważaj, że powinieneś być zainteresowany właściwością IsFaulted zadania. To dość brzydki temat, o którym możesz przeczytać więcej w tym artykule w bibliotece MSDN . Kiedy już sobie z tym poradzisz, powinieneś mieć również dobre miejsce w swoim kodzie, aby pozbyć się zadań.


6
Ale zadanie Threadw większości przypadków nie tworzy , używa ThreadPool.
svick

-1

Chciałbym zobaczyć, jak ktoś rozważa technikę pokazaną w tym poście: Typesafe, uruchom i zapomnij asynchroniczne wywołanie delegata w C #

Wygląda na to, że prosta metoda rozszerzenia poradzi sobie ze wszystkimi trywialnymi przypadkami interakcji z zadaniami i będzie w stanie wywołać metodę dispose na jej temat.

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
Oczywiście to nie pozwala pozbyć się Taskinstancji zwróconej przez ContinueWith, ale zobacz cytat Stephena Touba jest akceptowaną odpowiedzią: nie ma nic do usunięcia, jeśli nic nie wykonuje blokującego oczekiwania na zadanie.
Richard

1
Jak wspomina Richard, ContinueWith (...) zwraca również drugi obiekt Task, który następnie nie jest usuwany.
Simon P Stevens

1
Tak więc kod ContinueWith jest w rzeczywistości gorszy niż redudant, ponieważ spowoduje utworzenie innego zadania, aby pozbyć się starego zadania. Z obecnego stanu rzeczy w zasadzie nie byłoby możliwe wprowadzenie czekania blokującego do tego bloku kodu, gdyby nie to, że delegat akcji, do którego został przekazany, próbował manipulować samymi Zadaniami, również prawda?
Chris Marisic,

1
Możesz użyć sposobu, w jaki lambdy przechwytują zmienne w nieco trudny sposób, aby zająć się drugim zadaniem. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth

@GideonEngelberth, który pozornie musiałby działać. Ponieważ disper nigdy nie powinien zostać usunięty przez GC, powinien pozostać ważny do momentu, gdy lambda wywoła samą siebie w celu usunięcia, zakładając, że odniesienie jest nadal ważne / wzruszenie ramionami. Może potrzebuje pustej próby / złapania wokół tego?
Chris Marisic
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.