Por que TaskScheduler.Current é o TaskScheduler padrão?
Biblioteca de Tarefas Paralelas é ótima e eu a usei bastante nos últimos meses. No entanto, há algo realmente me incomodando: o fato de queTaskScheduler.Current
é o planejador de tarefas padrão, nãoTaskScheduler.Default
. Isso não é absolutamente óbvio à primeira vista na documentação nem nas amostra
Current
pode levar a erros sutis, pois seu comportamento está mudando dependendo de você estar em outra tarefa. O que não pode ser determinado facilmente.
Suponha que eu esteja gravando uma biblioteca de métodos assíncronos, usando o padrão assíncrono padrão com base em eventos para sinalizar a conclusão no contexto de sincronização original, exatamente da mesma maneira que os métodos XxxAsync fazem no .NET Framework (por exemplo,DownloadFileAsync
). Decido usar a Biblioteca Paralela de Tarefas para implementação, porque é realmente fácil implementar esse comportamento com o seguinte código:
public class MyLibrary {
public event EventHandler SomeOperationCompleted;
private void OnSomeOperationCompleted() {
var handler = SomeOperationCompleted;
if (handler != null)
handler(this, EventArgs.Empty);
}
public void DoSomeOperationAsync() {
Task.Factory
.StartNew
(
() => Thread.Sleep(1000) // simulate a long operation
, CancellationToken.None
, TaskCreationOptions.None
, TaskScheduler.Default
)
.ContinueWith
(t => OnSomeOperationCompleted()
, TaskScheduler.FromCurrentSynchronizationContext()
);
}
}
Até agora, tudo funciona bem. Agora, vamos fazer uma chamada para esta biblioteca clicando em um botão em um aplicativo WPF ou WinForms:
private void Button_OnClick(object sender, EventArgs args) {
var myLibrary = new MyLibrary();
myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
myLibrary.DoSomeOperationAsync();
}
private void DoSomethingElse() {
...
Task.Factory.StartNew(() => Thread.Sleep(5000)/*simulate a long operation*/);
...
}
Aqui, a pessoa que escreveu a chamada da biblioteca escolheu iniciar um novoTask
quando a operação for concluída. Nada incomum. Ele segue exemplos encontrados em qualquer lugar da Web e simplesmente usaTask.Factory.StartNew
sem especificar oTaskScheduler
(e não há sobrecarga fácil para especificar no segundo parâmetro). ODoSomethingElse
método @ funciona bem quando chamado sozinho, mas assim que é invocado pelo evento, a interface do usuário congela desdeTaskFactory.Current
reutilizará o agendador de tarefas do contexto de sincronização da continuação da minha bibliotec
Encontrar isso pode levar algum tempo, especialmente se a segunda chamada de tarefa estiver oculta em uma pilha de chamadas complexa. Obviamente, a correção aqui é simples quando você sabe como tudo funciona: sempre especifiqueTaskScheduler.Default
para qualquer operação que você espera executar no pool de threads. No entanto, talvez a segunda tarefa seja iniciada por outra biblioteca externa, sem saber sobre esse comportamento e usando ingenuamenteStartNew
sem um agendador específico. Espero que este caso seja bastante comu
Depois de entender, não consigo entender a escolha da equipe que está escrevendo o TPL para usarTaskScheduler.Current
ao invés deTaskScheduler.Default
como padrão:
Default
não é o padrão! E a documentação está seriamente faltando.O verdadeiro agendador de tarefas usado porCurrent
depende da pilha de chamadas! É difícil manter invariantes com esse comportamento. É complicado especificar o agendador de tarefas comStartNew
desde que você precise especificar as opções de criação de tarefas e o token de cancelamento primeiro, levando a linhas longas e menos legíveis. Isso pode ser aliviado escrevendo um método de extensão ou criando umTaskFactory
que usaDefault
. captura da pilha de chamadas tem custos adicionais de desempenh Quando realmente quero que uma tarefa dependa de outra tarefa em execução pai, prefiro especificá-la explicitamente para facilitar a leitura do código, em vez de confiar na mágica da pilha de chamadaEu sei que essa pergunta pode parecer bastante subjetiva, mas não consigo encontrar um bom argumento objetivo sobre o porquê desse comportamento. Tenho certeza de que estou perdendo algo aqui: é por isso que estou me voltando para você.