Grãos e tarefas externas
Por definição, todas as subtarefas geradas pelo código de grão (por exemplo, usando await
, ContinueWith
ou Task.Factory.StartNew
) serão enviadas na mesma TaskScheduler por ativação que a tarefa pai e, portanto, herdarão o mesmo modelo de execução de thread único que o restante do código de grão. Este é o ponto principal por trás da execução de thread único da simultaneidade baseada em turnos de grãos.
Em alguns casos, o código de grão pode precisar "sair" do modelo de agendamento de tarefas do Orleans "fazer algo especial", como apontar explicitamente um Task
para um agendador de tarefas diferente ou para o .NET ThreadPool. Um exemplo desse caso é quando o código de grão precisa executar uma chamada de bloqueio remoto síncrona (como E/S remota). Executar essa chamada de bloqueio no contexto do grão bloqueará o grão e, portanto, não é recomendado. Em vez disso, o código de grão pode executar essa parte do código de bloqueio no thread do pool de threads e unir (await
) a conclusão dessa execução para prosseguir no contexto do grão. Esperamos que evitar o agendador do Orleans seja um cenário de uso muito avançado e raramente necessário, além dos padrões de uso "normais".
APIs baseadas em tarefa
await, TaskFactory.StartNew (confira abaixo), Task.ContinueWith, Task.WhenAny, Task.WhenAll, Task.Delay, todos esses itens respeitam o agendador de tarefas atual. Isso significa que usá-los da maneira padrão, sem transmitir um TaskScheduler diferente, fará com que eles sejam executados no contexto do grão.
Tanto Task.Run quanto o representante de
endMethod
do TaskFactory.FromAsyncnão respeitam o agendador de tarefas atual. Ambos usam o agendadorTaskScheduler.Default
, que é o agendador de tarefas do pool de threads do .NET. Portanto, o código dentro deTask.Run
e oendMethod
emTask.Factory.FromAsync
sempre serão executados no pool de threads do .NET fora do modelo de execução de thread único para grãos do Orleans. No entanto, qualquer código apósawait Task.Run
ouawait Task.Factory.FromAsync
será executado novamente no agendador no momento em que a tarefa foi criada, que é o agendador do grão.Task.ConfigureAwait com
false
é uma API explícita para escapar do agendador de tarefas atual. Isso fará com que o código após uma tarefa aguardada seja executado no agendador TaskScheduler.Default, que é o pool de threads do .NET e, assim, interrompa a execução de thread único do grão.Cuidado
Em geral, você nunca deve usar
ConfigureAwait(false)
diretamente no código de grão.Métodos com a assinatura
async void
não devem ser usados com grãos. Eles são destinados a manipuladores de eventos da interface gráfica do usuário. O métodoasync void
poderá travar imediatamente o processo atual se for permitido que uma exceção escape, sem nenhuma maneira de lidar com ela. Isso também é verdade paraList<T>.ForEach(async element => ...)
e qualquer outro método que aceite um Action<T>, pois o representante assíncrono será coagido em um representanteasync void
.
Representantes Task.Factory.StartNew
e async
A recomendação usual para agendar tarefas em qualquer programa em C# é usar Task.Run
em vez de Task.Factory.StartNew
. Uma rápida pesquisa no Google sobre o uso de Task.Factory.StartNew
sugere que ele é perigoso e que deve-se sempre favorecer o uso de Task.Run
. Mas, para permanecer no modelo de execução de thread único do grão, precisamos usá-lo. Como fazemos isso corretamente? O perigo ao usar Task.Factory.StartNew()
é que ele não dá suporte nativo a representantes assíncronos. Isso significa que isso é provavelmente um bug: var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)
. notIntendedTask
não é uma tarefa que é concluída ao mesmo tempo que SomeDelegateAsync
. Em vez disso, deve-se sempre desencapsular a tarefa retornada: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()
.
Exemplo de diversas tarefas e o agendador de tarefas
Veja abaixo o código de exemplo que demonstra o uso de TaskScheduler.Current
, Task.Run
e de um agendador personalizado especial para escapar do contexto de grão do Orleans e como voltar a ele.
public async Task MyGrainMethod()
{
// Grab the grain's task scheduler
var orleansTS = TaskScheduler.Current;
await Task.Delay(10_000);
// Current task scheduler did not change, the code after await is still running
// in the same task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
Task t1 = Task.Run(() =>
{
// This code runs on the thread pool scheduler, not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
});
await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in Orleans task scheduler context, we are now back
// to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
// Example of using Task.Factory.StartNew with a custom scheduler to escape from
// the Orleans scheduler
Task t2 = Task.Factory.StartNew(() =>
{
// This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
// the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
},
CancellationToken.None,
TaskCreationOptions.None,
scheduler: MyCustomSchedulerThatIWroteMyself);
await t2;
// We are back to Orleans task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
Exemplo de uma chamada de grão do código que é executado em um pool de threads
Outro cenário é um fragmento de código de grão que precisa "sair" do modelo de agendamento de tarefas desse grão e ser executado em um pool de threads (ou algum outro contexto que não seja o grão), mas ainda precisa chamar outro grão. As chamadas de grão podem ser feitas em contextos que não são do grão sem cerimônia extra.
Veja a seguir um código que demonstra como uma chamada de grão pode ser feita com base em um fragmento de código executado dentro de um grão, mas não no contexto desse grão.
public async Task MyGrainMethod()
{
// Grab the Orleans task scheduler
var orleansTS = TaskScheduler.Current;
var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
Task<int> t1 = Task.Run(async () =>
{
// This code runs on the thread pool scheduler,
// not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
int res = await fooGrain.MakeGrainCall();
// This code continues on the thread pool scheduler,
// not on the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
return res;
});
int result = await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in the Orleans task scheduler context,
// we are now back to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
Trabalhar com bibliotecas
Algumas bibliotecas externas que seu código está usando podem estar utilizando ConfigureAwait(false)
internamente. É uma prática boa e correta no .NET usar ConfigureAwait(false)
ao implementar bibliotecas de uso geral. Isso não é um problema no Orleans. Contanto que o código no grão que invoca o método da biblioteca esteja aguardando a chamada da biblioteca com um await
regular, o código do grão está correto. O resultado será exatamente o desejado: o código da biblioteca executará continuações no agendador padrão (o valor retornado por TaskScheduler.Default
, que não garante que as continuações serão executadas em um thread ThreadPool, pois elas geralmente são integradas no thread anterior) e o código do grão será executado no agendador do grão.
Outra pergunta frequente é se há necessidade de executar chamadas de biblioteca com Task.Run
, ou seja, se há necessidade de descarregar explicitamente o código da biblioteca em ThreadPool
(para o código de grão fazer Task.Run(() => myLibrary.FooAsync())
). A resposta é não. Não há necessidade de descarregar nenhum código em ThreadPool
, exceto no caso do código de biblioteca que está fazendo um bloqueio de chamadas síncronas. Normalmente, qualquer biblioteca assíncrona .NET bem escrita e correta (métodos que retornam Task
e são nomeados com um sufixo Async
) não faz chamadas de bloqueio. Portanto, não há necessidade de descarregar nada em ThreadPool
, a menos que você suspeite que a biblioteca assíncrona esteja com bugs ou esteja usando deliberadamente uma biblioteca de bloqueio síncrona.
Deadlocks
Como os grãos são executados em um único thread, é possível utilizar deadlock em um grão bloqueando-o de maneira síncrona a fim de exigir diversos threads para o desbloqueio. Isso significa que o código que chama qualquer um dos métodos e propriedades a seguir pode utilizar deadlock em um grão quando as tarefas fornecidas ainda não foram concluídas no momento em que o método ou propriedade é invocado:
Task.Wait()
Task.Result
Task.WaitAny(...)
Task.WaitAll(...)
task.GetAwaiter().GetResult()
Esses métodos devem ser evitados em qualquer serviço de alta simultaneidade porque podem levar a um desempenho insatisfatório e em instabilidade ao privar o ThreadPool
do .NET bloqueando threads que poderiam fazer um trabalho útil e exigir que o ThreadPool
do .NET injete threads adicionais para a conclusão. Ao executar o código de grão, esses métodos, conforme mencionado acima, podem causar um deadlock no grão e, portanto, também devem ser evitados no código de grão.
Caso haja algum trabalho de sync-over-async que não possa ser evitado, é melhor mover esse trabalho para um agendador separado. A maneira mais simples de fazer isso é usar await Task.Run(() => task.Wait())
, por exemplo. Observe que é altamente recomendável evitar o trabalho de sync-over-async, pois, conforme mencionado acima, isso prejudicará a escalabilidade e o desempenho do aplicativo.
Resumo do trabalho com tarefas no Orleans
O que você está tentando fazer? | Como fazer |
---|---|
Execute o trabalho em segundo plano em threads do pool de threads do .NET. Nenhum código de grão ou chamada de grão é permitido. | Task.Run |
Execute a tarefa de trabalho assíncrona no código de grão com garantias de simultaneidade baseadas em turnos do Orleans (veja acima). | Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap) |
Execute a tarefa de trabalho síncrona no código de grão com garantias de simultaneidade baseadas em turnos do Orleans. | Task.Factory.StartNew(WorkerSync) |
Tempos limite de execução de itens de trabalho | Task.Delay + Task.WhenAny |
Chamar um método de biblioteca assíncrona | await a chamada de biblioteca |
Use async /await |
O modelo normal de programação assíncrona de tarefas do .NET. Compatível e recomendado |
ConfigureAwait(false) |
Não use dentro do código de grão. Permitido somente dentro de bibliotecas. |