Tarefas externas e grãos
Por design, todas as subtarefas geradas a partir do código grain (por exemplo, usando await
ou ContinueWith
Task.Factory.StartNew
ou ) serão despachadas na mesma ativação TaskScheduler que a tarefa pai e, portanto, herdarão o mesmo modelo de execução single-threaded que o restante do código grain. Este é o principal ponto por trás da execução single-threaded da simultaneidade baseada em turnos de grãos.
Em alguns casos, o código grain pode precisar "quebrar" o Orleans modelo de agendamento de tarefas e "fazer algo especial", como apontar explicitamente para Task
um agendador de tarefas diferente ou para o .NET ThreadPool. Um exemplo desse caso é quando o código grain tem que executar uma chamada de bloqueio remoto síncrona (como E/S remota). Executar essa chamada de bloqueio no contexto de grão bloqueará o grão e, portanto, nunca deve ser feito. Em vez disso, o código grain pode executar essa parte do código de bloqueio no thread do pool de threads e unir (await
) a conclusão dessa execução e prosseguir no contexto de grão. Esperamos que escapar do agendador seja um cenário de Orleans uso muito avançado e raramente necessário, além dos padrões de uso "normais".
APIs baseadas em tarefas
aguardar, TaskFactory.StartNew (veja abaixo), Task.ContinueWith, , Task.WhenAllTask.WhenAny, todos Task.Delay respeitam o agendador de tarefas atual. Isso significa que usá-los da maneira padrão, sem passar um diferente TaskScheduler, fará com que eles sejam executados no contexto de grão.
Ambos Task.Run e o
endMethod
delegado de não respeitam o agendador de TaskFactory.FromAsync tarefas atual. Ambos usam oTaskScheduler.Default
agendador, que é o agendador de tarefas do pool de threads .NET. Portanto, o código dentroTask.Run
e oendMethod
inTask.Factory.FromAsync
sempre serão executados no pool de threads do .NET fora do modelo de execução de thread único para Orleans grãos. No entanto, qualquer código após oawait Task.Run
ouawait Task.Factory.FromAsync
será executado novamente sob o agendador no ponto em que a tarefa foi criada, que é o agendador do grão.Task.ConfigureAwait with
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 TaskScheduler.Default agendador, que é o pool de threads do .NET, e, portanto, interromperá a execução de thread único do grão.Atenção
Em geral , você nunca deve usar
ConfigureAwait(false)
diretamente no código de grãos.Métodos com a assinatura
async void
não devem ser usados com grãos. Eles são destinados a manipuladores de eventos de interface gráfica do usuário.async void
método pode falhar imediatamente o processo atual se eles permitirem que uma exceção escape, sem nenhuma maneira de lidar com a exceção. Isso também é verdade paraList<T>.ForEach(async element => ...)
qualquer outro método que aceite um Action<T>, uma vez que o delegado assíncrono será coagido a umasync void
delegado.
Task.Factory.StartNew
e async
delegados
A recomendação usual para agendar tarefas em qualquer programa C# é usar Task.Run
em favor do Task.Factory.StartNew
. Uma rápida pesquisa no google sobre o uso de vai sugerir que é perigoso e que se deve sempre favorecerTask.Run
.Task.Factory.StartNew
Mas se quisermos permanecer no modelo de execução single-threaded do grão para o nosso grão, então precisamos usá-lo, então como fazê-lo corretamente? O perigo ao usar Task.Factory.StartNew()
é que ele não suporta nativamente delegados assíncronos. Isto significa que este é provavelmente um bug: var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)
. notIntendedTask
não é uma tarefa que é concluída quando SomeDelegateAsync
o faz. Em vez disso, deve-se sempre desembrulhar a tarefa retornada: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()
.
Exemplo de várias tarefas e o agendador de tarefas
Abaixo está o código de exemplo que demonstra o uso de TaskScheduler.Current
, Task.Run
e um agendador personalizado especial para escapar do contexto de Orleans grãos 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 fazer uma chamada de grão a partir de código que é executado em um pool de threads
Outro cenário é um pedaço de código de grão que precisa "sair" do modelo de agendamento de tarefas do grão e executar em um pool de threads (ou algum outro contexto não granulado), mas ainda precisa chamar outro grão. As chamadas de grãos podem ser feitas a partir de contextos sem grãos, sem cerimônia extra.
A seguir está o código que demonstra como uma chamada de grão pode ser feita a partir de um pedaço de código que é executado dentro de um grão, mas não no contexto de 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 usando ConfigureAwait(false)
internamente. É uma prática boa e correta no .NET para usar ConfigureAwait(false)
ao implementar bibliotecas de uso geral. Este não é um problema na Orleans. Contanto que o código no grain que invoca o método library esteja aguardando a chamada de biblioteca com um regular await
, o código grain está correto. O resultado será exatamente como desejado – o código da biblioteca executará continuações no agendador padrão (o valor retornado pelo TaskScheduler.Default
, o que não garante que as continuações serão executadas em um ThreadPool thread, pois as continuações geralmente estão embutidas no thread anterior), enquanto o código grain será executado no agendador do grão.
Outra pergunta freqüente é se há necessidade de executar chamadas de biblioteca com Task.Run
—ou seja, se há necessidade de descarregar explicitamente o código da biblioteca para ThreadPool
(para que o código grain faça Task.Run(() => myLibrary.FooAsync())
). A resposta é não. Não há necessidade de descarregar nenhum código, ThreadPool
exceto no caso do código da 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 Async
sufixo) não faz chamadas de bloqueio. Assim, não há necessidade de descarregar nada, a ThreadPool
menos que você suspeite que a biblioteca assíncrona está com bugs ou se você estiver usando deliberadamente uma biblioteca de bloqueio síncrona.
Impasses
Como os grãos são executados de forma de thread único, é possível bloquear um grão bloqueando de forma síncrona de uma forma que exigiria vários threads para desbloquear. Isso significa que o código que chama qualquer um dos seguintes métodos e propriedades pode bloquear um grão se as tarefas fornecidas ainda não tiverem sido concluídas no momento em que o método ou propriedade for 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 instabilidade ao privar o .NET ThreadPool
, bloqueando threads que poderiam estar executando trabalho útil e exigindo que o .NET ThreadPool
injete threads adicionais para que eles possam ser concluídos. Ao executar o código de grão, esses métodos, como mencionado acima, podem causar o bloqueio do grão e, portanto, também devem ser evitados no código de grãos.
Se houver algum trabalho de sincronização sobre assíncrono 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 sincronização sobre assíncrono , pois, como mencionado acima, isso prejudicará a escalabilidade e o desempenho do seu aplicativo.
Resumo do trabalho com tarefas em Orleans
O que está a tentar fazer? | Como fazê-lo |
---|---|
Execute o trabalho em segundo plano em threads do pool de threads do .NET. Nenhum código de grão ou chamadas de grãos são permitidos. | Task.Run |
Execute tarefas de trabalho assíncronas a partir do código grain com Orleans garantias de simultaneidade baseadas em turnos (veja acima). | Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap) |
Execute tarefas de trabalho síncronas a partir do código grain com Orleans garantias de simultaneidade baseadas em turnos. | Task.Factory.StartNew(WorkerSync) |
Tempos limite para execução de itens de trabalho | Task.Delay + Task.WhenAny |
Chamar um método de biblioteca assíncrona | await a chamada da biblioteca |
Utilizar o comando async /await |
O modelo de programação .NET Task-Async normal. Suportado & recomendado |
ConfigureAwait(false) |
Não use dentro do código de grão. Permitido apenas dentro de bibliotecas. |