È considerato accettabile non chiamare Dispose () su un oggetto Task TPL?


123

Voglio attivare un'attività da eseguire su un thread in background. Non voglio aspettare il completamento delle attività.

In .net 3.5 avrei fatto questo:

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

In .net 4 il TPL è il modo suggerito. Lo schema comune che ho visto consigliato è:

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

Tuttavia, il StartNew()metodo restituisce un Taskoggetto che implementa IDisposable. Questo sembra essere trascurato dalle persone che raccomandano questo modello. La documentazione MSDN sul Task.Dispose()metodo dice:

"Chiama sempre Dispose prima di rilasciare il tuo ultimo riferimento all'attività."

Non è possibile chiamare dispose su un'attività finché non viene completata, quindi avere il thread principale in attesa e chiamare dispose annullerebbe il punto di eseguire su un thread in background in primo luogo. Inoltre non sembra esserci alcun evento completato / finito che potrebbe essere utilizzato per la pulizia.

La pagina MSDN sulla classe Task non commenta questo aspetto e il libro "Pro C # 2010 ..." consiglia lo stesso modello e non fa alcun commento sull'eliminazione delle attività.

So che se lo lascio, il finalizzatore lo catturerà alla fine, ma questo tornerà e mi morderà quando farò molte attività di fuoco e dimentico come questa e il thread del finalizzatore viene sopraffatto?

Quindi le mie domande sono:

  • È accettabile non invitare Dispose()la Taskclasse in questo caso? E se sì, perché e ci sono rischi / conseguenze?
  • C'è qualche documentazione che discute questo?
  • O c'è un modo appropriato per smaltire l' Taskoggetto che mi sono perso?
  • O c'è un altro modo per fare fuoco e dimenticare le attività con il TPL?

Risposte:


108

C'è una discussione su questo nei forum MSDN .

Stephen Toub, un membro del team Microsoft pfx ha questo da dire:

Task.Dispose esiste a causa di Task che potenzialmente esegue il wrapping di un handle di evento utilizzato durante l'attesa del completamento dell'attività, nel caso in cui il thread in attesa debba effettivamente bloccarsi (al contrario di girare o potenzialmente eseguire l'attività su cui è in attesa). Se tutto ciò che stai facendo è usare le continuazioni, l'handle dell'evento non verrà mai assegnato
...
è probabilmente meglio fare affidamento sulla finalizzazione per occuparsi delle cose.

Aggiornamento (ottobre 2012)
Stephen Toub ha pubblicato un blog intitolato Devo smaltire le attività? che fornisce qualche dettaglio in più e spiega i miglioramenti in .Net 4.5.

In sintesi: non è necessario smaltire gli Taskoggetti il ​​99% delle volte.

Ci sono due ragioni principali per smaltire un oggetto: liberare risorse non gestite in modo tempestivo e deterministico ed evitare il costo di esecuzione del finalizzatore dell'oggetto. Nessuno di questi si applica alla Taskmaggior parte delle volte:

  1. A partire da .Net 4.5, l'unica volta che a Taskalloca l'handle di attesa interno (l'unica risorsa non gestita Tasknell'oggetto) è quando si utilizza esplicitamente il IAsyncResult.AsyncWaitHandledi Task, e
  2. L' Taskoggetto stesso non ha un finalizzatore; l'handle è esso stesso avvolto in un oggetto con un finalizzatore, quindi a meno che non sia allocato, non ci sono finalizzatori da eseguire.

3
Grazie, interessante. Tuttavia, va contro la documentazione di MSDN. C'è qualche parola ufficiale da parte di MS o del team .net che questo è un codice accettabile. C'è anche il punto sollevato alla fine di quella discussione che "cosa succede se l'implementazione cambia in una versione futura"
Simon P Stevens

In realtà, ho appena notato che il risponditore in quel thread funziona davvero in Microsoft, apparentemente nel team pfx, quindi suppongo che questa sia una sorta di risposta ufficiale. Ma c'è un suggerimento verso il fondo che non funziona in tutti i casi. Se c'è una potenziale perdita, è meglio tornare a ThreadPool.QueueUserWorkItem che so essere sicuro?
Simon P Stevens

Sì, è molto strano che ci sia un Dispose che potresti non chiamare. Se dai un'occhiata agli esempi qui msdn.microsoft.com/en-us/library/dd537610.aspx e qui msdn.microsoft.com/en-us/library/dd537609.aspx non stanno eliminando le attività. Tuttavia gli esempi di codice in MSDN a volte dimostrano tecniche pessime. Anche il ragazzo che ha risposto alla domanda funziona per Microsoft.
Kirill Muzykov

2
@ Simon: (1) Il documento MSDN che citi è il consiglio generico, casi specifici hanno consigli più specifici (ad esempio, non è necessario utilizzarlo EndInvokein WinForms quando si utilizza BeginInvokeper eseguire codice sul thread dell'interfaccia utente). (2) Stephen Toub è abbastanza conosciuto come un normale oratore sull'uso efficace di PFX (ad esempio su channel9.msdn.com ), quindi se qualcuno può dare una buona guida, allora è lui. Nota il suo secondo paragrafo: ci sono volte che lasciare le cose al finalizzatore è meglio.
Richard

12

Questo è lo stesso tipo di problema della classe Thread. Consuma 5 handle del sistema operativo ma non implementa IDisposable. Buona decisione dei progettisti originali, ovviamente ci sono pochi modi ragionevoli per chiamare il metodo Dispose (). Dovresti prima chiamare Join ().

La classe Task aggiunge un handle a questo, un evento di ripristino manuale interno. Qual è la risorsa del sistema operativo più economica che ci sia. Ovviamente, il suo metodo Dispose () può rilasciare solo quell'handle di evento, non i 5 handle consumati da Thread. Sì, non preoccuparti .

Fai attenzione che dovresti essere interessato alla proprietà IsFaulted dell'attività. È un argomento piuttosto brutto, puoi saperne di più in questo articolo di MSDN Library . Una volta affrontato correttamente questo problema, dovresti anche avere un buon posto nel tuo codice per smaltire le attività.


6
Ma un'attività non crea un Threadnella maggior parte dei casi, utilizza ThreadPool.
svick

-1

Mi piacerebbe vedere qualcuno soppesare la tecnica mostrata in questo post: Typesafe fire-and-forget asynchronous delegate invocation in C #

Sembra che un semplice metodo di estensione gestirà tutti i casi banali di interazione con le attività e sarà in grado di chiamare dispose su di esso.

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
Ovviamente ciò non riesce a smaltire l' Taskistanza restituita da ContinueWith, ma vedere la citazione di Stephen Toub è la risposta accettata: non c'è nulla da eliminare se nulla esegue un'attesa di blocco su un'attività.
Richard

1
Come menzionato da Richard, ContinueWith (...) restituisce anche un secondo oggetto Task che quindi non viene eliminato.
Simon P Stevens

1
Così come questo è il codice ContinueWith è in realtà peggio di ridudente poiché causerà la creazione di un'altra attività solo per smaltire la vecchia attività. Con il modo in cui sta questo, fondamentalmente non sarebbe possibile introdurre un'attesa di blocco in questo blocco di codice a parte se il delegato dell'azione che hai passato stava tentando di manipolare anche Tasks stesso?
Chris Marisic

1
Potresti essere in grado di utilizzare il modo in cui lambda acquisiscono le variabili in un modo leggermente complicato per occuparti della seconda attività. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth

@GideonEngelberth che apparentemente dovrebbe funzionare. Dal momento che disper non dovrebbe mai essere eliminato dal GC, dovrebbe rimanere valido fino a quando lambda non invoca se stesso per smaltire, supponendo che il riferimento sia ancora valido qui / scrollata di spalle. Forse ha bisogno di una prova / cattura vuota intorno ad esso?
Chris Marisic
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.