É considerado aceitável não chamar Dispose () em um objeto de tarefa TPL?

123

Quero desencadear uma tarefa para executar em um thread em segundo plano. Não quero esperar a conclusão das tarefas.

No .net 3.5, eu teria feito isso:

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

No .net 4, o TPL é a maneira sugerida. O padrão comum que eu vi recomendado é:

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

No entanto, o StartNew()método retorna um Taskobjeto que implementa IDisposable. Isso parece ser esquecido pelas pessoas que recomendam esse padrão. A documentação do MSDN sobre o Task.Dispose()método diz:

"Sempre chame Dispose antes de liberar sua última referência para a tarefa."

Você não pode chamar a disposição em uma tarefa até que ela seja concluída, portanto, fazer com que o encadeamento principal aguarde e descarte a chamada acabaria com o ponto de execução em um encadeamento em segundo plano. Também não parece haver nenhum evento concluído / finalizado que possa ser usado para limpeza.

A página MSDN na classe Task não comenta isso, e o livro "Pro C # 2010 ..." recomenda o mesmo padrão e não faz nenhum comentário sobre o descarte de tarefas.

Eu sei que se eu deixar o finalizador irá pegá-lo no final, mas isso vai voltar e me morder quando estou fazendo muito fogo e esquecer tarefas como essa e o encadeamento do finalizador ficar sobrecarregado?

Então, minhas perguntas são:

  • É aceitável não chamar Dispose()a Taskturma neste caso? E se sim, por que e há riscos / consequências?
  • Existe alguma documentação que discuta isso?
  • Ou existe uma maneira apropriada de descartar o Taskobjeto que perdi?
  • Ou existe outra maneira de fazer fogo e esquecer tarefas com o TPL?
Simon P Stevens
fonte
1
Relacionado: A maneira correta de atear fogo e esquecer (ver resposta )
Simon P Stevens

Respostas:

108

Há uma discussão sobre isso nos fóruns do MSDN .

Stephen Toub, membro da equipe Microsoft pfx, tem o seguinte a dizer:

Task.Dispose existe devido ao fato de a Tarefa encapsular um identificador de evento usado ao aguardar a conclusão da tarefa, no caso em que o encadeamento em espera realmente precisa ser bloqueado (em vez de girar ou executar potencialmente a tarefa em que está aguardando). Se tudo o que você está fazendo é usar continuações, esse identificador de evento nunca será alocado
...
provavelmente é melhor confiar na finalização para cuidar das coisas.

Atualização (outubro de 2012)
Stephen Toub postou um blog intitulado Preciso descartar as tarefas? que fornece mais detalhes e explica as melhorias no .Net 4.5.

Em resumo: você não precisa descartar Taskobjetos 99% do tempo.

Há dois motivos principais para descartar um objeto: liberar recursos não gerenciados de maneira determinística e oportuna e evitar o custo de executar o finalizador do objeto. Nenhum destes se aplica à Taskmaior parte do tempo:

  1. A partir do .Net 4.5, a única vez que um Taskaloca o identificador de espera interno (o único recurso não gerenciado no Taskobjeto) é quando você usa explicitamente o IAsyncResult.AsyncWaitHandlede Task, e
  2. O Taskobjeto em si não possui um finalizador; o próprio identificador está envolvido em um objeto com um finalizador; portanto, a menos que esteja alocado, não há finalizador para executar.
Kirill Muzykov
fonte
3
Obrigado, interessante. Isso vai contra a documentação do MSDN. Existe alguma palavra oficial da MS ou da equipe .net de que este é um código aceitável. Há também o ponto levantado no final da discussão de que "e se a implementação mudar em uma versão futura"
Simon P Stevens
Na verdade, acabei de perceber que o respondente nesse segmento realmente funciona na microsoft, aparentemente na equipe pfx, então suponho que essa seja uma resposta oficial das sortes. Mas há sugestões para o fundo, que não funcionam em todos os casos. Se houver um vazamento em potencial, é melhor reverter para ThreadPool.QueueUserWorkItem que sei que é seguro?
Simon P Stevens
Sim, é muito estranho que exista um Dispose que você não possa chamar. Se você der uma olhada nas amostras aqui msdn.microsoft.com/en-us/library/dd537610.aspx e aqui msdn.microsoft.com/en-us/library/dd537609.aspx, elas não estão descartando tarefas. No entanto, exemplos de código no MSDN às vezes demonstram técnicas muito ruins. Também o cara respondeu à pergunta funciona para a Microsoft.
Kirill Muzykov
2
@ Simon: (1) O documento do MSDN que você cita é o conselho genérico, casos específicos têm conselhos mais específicos (por exemplo, não é necessário usar o EndInvokeWinForms BeginInvokeao executar o código no thread da interface do usuário). (2) Stephen Toub é bastante conhecido como orador regular sobre o uso efetivo do PFX (por exemplo, no channel9.msdn.com ), portanto, se alguém pode dar uma boa orientação, então é ele. Observe seu segundo parágrafo: há momentos em que deixar as coisas para o finalizador é melhor.
Richard
12

Esse é o mesmo tipo de problema da classe Thread. Consome 5 identificadores do sistema operacional, mas não implementa IDisposable. Boa decisão dos designers originais, é claro que existem poucas maneiras razoáveis ​​de chamar o método Dispose (). Você teria que ligar para Join () primeiro.

A classe Task adiciona um identificador a isso, um evento de redefinição manual interna. Qual é o recurso mais barato do sistema operacional que existe. Obviamente, seu método Dispose () pode liberar apenas esse identificador de evento, não os 5 identificadores que o Thread consome. Sim, não se preocupe .

Lembre-se de que você deve estar interessado na propriedade IsFaulted da tarefa. É um tópico bastante feio, você pode ler mais sobre isso neste artigo da MSDN Library . Depois de lidar com isso corretamente, você também deve ter uma boa localização no seu código para descartar as tarefas.

Hans Passant
fonte
6
Mas uma tarefa não cria um Threadna maioria dos casos, ela usa o ThreadPool.
svick
-1

Eu adoraria ver alguém avaliar a técnica mostrada nesta postagem: Typesafe invocação assíncrona de delegado assíncrona em C #

Parece que um método de extensão simples lida com todos os casos triviais de interação com as tarefas e pode ser descartado.

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());
}
Chris Marisic
fonte
3
É claro que isso falha em descartar a Taskinstância retornada por ContinueWith, mas veja a citação de Stephen Toub é a resposta aceita: não há nada a descartar se nada executar uma espera de bloqueio em uma tarefa.
Richard
1
Como Richard menciona, o ContinueWith (...) também retorna um segundo objeto Task, que não é descartado.
Simon P Stevens
1
Portanto, o código ContinueWith, na verdade, é pior do que redundante, pois fará com que outra tarefa seja criada apenas para descartar a tarefa antiga. Do jeito que está, basicamente não seria possível introduzir uma espera de bloqueio nesse bloco de código, exceto se o delegado de ação que você passou nele estava tentando manipular as próprias tarefas também está correto?
Chris Marisic
1
Você pode usar como os lambdas capturam variáveis ​​de uma maneira um pouco complicada para cuidar da segunda tarefa. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth
@GideonEngelberth que aparentemente teria que funcionar. Como o disper nunca deve ser descartado pelo GC, ele deve permanecer válido até que o lambda se invoque para descartá-lo, assumindo que a referência ainda seja válida lá / encolher de ombros. Talvez precise de uma tentativa / captura vazia em torno dele?
Chris Marisic