Partilhar via


Tarefas externas e grãos

Por design, todas as subtarefas geradas a partir do código grain (por exemplo, usando await, ContinueWithou Task.Factory.StartNew) são despachadas na mesma ativação TaskScheduler que a tarefa pai. Portanto, eles herdam o mesmo modelo de execução de thread único que o resto do código de grão. 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 "sair" do modelo de agendamento de tarefas e "fazer algo especial", como apontar explicitamente um Orleans para um agendador de tarefas diferente ou para o .NET Task. Um exemplo é quando o código grain precisa executar uma chamada de bloqueio remoto síncrona (como E/S remota). Executar essa chamada de bloqueio no contexto de grão bloqueia 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 num thread de um pool de threads, aguardar (await) a conclusão dessa execução e, em seguida, prosseguir no contexto do grain. Espera-se que escapar do agendador seja um cenário de uso muito avançado e raramente necessário, além dos Orleans padrões de uso típicos.

APIs baseadas em tarefas

  1. await, TaskFactory.StartNew (veja abaixo), Task.ContinueWith, , Task.WhenAnyTask.WhenAll, e Task.Delay todos respeitam o agendador de tarefas atual. Isso significa usá-los da maneira padrão, sem passar um TaskScheduler diferente, resulta na sua execução no contexto granular.

  2. Ambos Task.Run e o endMethod delegado de não TaskFactory.FromAsync respeitam o agendador de tarefas atual. Ambos usam o TaskScheduler.Default agendador, agendador de tarefas do pool de threads do .NET. Portanto, o código dentro Task.Run e o endMethod in Task.Factory.FromAsyncsempre sã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 depois await Task.Run ou await Task.Factory.FromAsync é executado novamente sob o agendador ativo no ponto em que a tarefa foi criada, que é o agendador associado ao grão.

  3. Task.ConfigureAwait with false é uma API explícita para escapar do agendador de tarefas atual. Faz com que o código após um aguardado Task seja executado no agendador TaskScheduler.Default (o pool de threads .NET), quebrando assim a execução single-threaded da tarefa.

    Atenção

    Geralmente, nunca use ConfigureAwait(false) diretamente no código granular.

  4. 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. Um async void método pode travar imediatamente o processo atual se permitir que uma exceção escape, sem nenhuma maneira de lidar com a exceção. Isso também se aplica a List<T>.ForEach(async element => ...) e a qualquer outro método que aceite um Action<T>, uma vez que o delegado assíncrono se converte em um async void delegado.

Task.Factory.StartNew e async delegados

A recomendação usual para agendar tarefas em C# é usar Task.Run em vez de Task.Factory.StartNew. Uma rápida pesquisa na web sugere Task.Factory.StartNewque é perigoso e favorecimento Task.Run é sempre recomendado. No entanto, para ficar dentro do modelo de execução single-threaded do grão, Task.Factory.StartNew deve ser usado. Então, como usá-lo corretamente? O perigo com Task.Factory.StartNew() é a sua falta de suporte nativo para delegados assíncronos. Isso significa que código como var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync) é provavelmente um bug. notIntendedTask não é uma tarefa concluída quando SomeDelegateAsync termina. Em vez disso, sempre desembrulhe a tarefa retornada: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap().

Exemplo: Várias tarefas e o agendador de tarefas

Abaixo está o código de exemplo demonstrando o uso de TaskScheduler.Current, Task.Run, e um agendador personalizado especial para escapar do contexto do Orleans grão e como retornar 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 para um grain a partir de código em execução numa thread do pool de threads

Outro cenário envolve o código de grain precisando "quebrar" o modelo de agendamento de tarefas do grain e ser executado numa thread de um pool de threads (ou algum outro contexto fora dos grains), mas ainda precisando chamar outro grain. As chamadas de grãos podem ser feitas a partir de contextos sem grãos, sem cerimônia extra.

O código a seguir demonstra como realizar uma chamada de grão a partir de código que está em execução no interior de um grão, mas fora do 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 usadas pelo código podem usar ConfigureAwait(false) internamente. Usar ConfigureAwait(false) é uma prática boa e correta no .NET ao implementar bibliotecas de uso geral. Isso não é um problema no Orleans. Contanto que o código grain invocando o método library aguarde a chamada da biblioteca com um regular await, o código grain está correto. O resultado é exatamente como desejado: o código da biblioteca executa continuações no agendador padrão (o valor retornado pelo TaskScheduler.Default, que não garante que as continuações sejam executadas em um thread ThreadPool, uma vez que frequentemente são integradas no thread anterior), enquanto o código de grão é executado no agendador do grão.

Outra pergunta freqüente é se as chamadas de biblioteca precisam ser executadas com Task.Run—ou seja, se o código da biblioteca precisa de descarregamento explícito para o ThreadPool (por exemplo, await Task.Run(() => myLibrary.FooAsync())). A resposta é não. O descarregamento do código para o ThreadPool não é necessário, exceto quando o código da biblioteca faz chamadas síncronas que bloqueiam. Normalmente, qualquer biblioteca assíncrona .NET bem escrita e correta (métodos retornando Task e nomeados com um Async sufixo) não faz chamadas de bloqueio. Assim, descarregar qualquer coisa para o ThreadPool não é necessário, a menos que a biblioteca assíncrona seja suspeita de ser problemática ou que se utilize deliberadamente uma biblioteca de bloqueio síncrona.

Impasses

Como os grãos executam threads únicos, o bloqueio de um grão é possível bloqueando de forma síncrona de uma forma que exija o desbloqueio de vários threads. Isso significa que o código que chama qualquer um dos seguintes métodos e propriedades pode causar uma situação de deadlock em um grão se as tarefas fornecidas não tiverem sido concluídas até ao momento em que o método ou propriedade seja invocado.

  • Task.Wait()
  • Task.Result
  • Task.WaitAny(...)
  • Task.WaitAll(...)
  • task.GetAwaiter().GetResult()

Evite esses métodos em qualquer serviço de alta simultaneidade, pois eles podem levar a um desempenho ruim e instabilidade. Eles privam o .NET ThreadPool bloqueando threads que poderiam executar trabalho útil e exigem a ThreadPool injeção de threads adicionais para conclusão. Ao executar o código de grão, esses métodos podem causar o bloqueio do grão, portanto, evite-os no código de grão também.

Se algum trabalho de sincronização sobre assíncrono for inevitável, mover esse trabalho para um agendador separado é melhor. A maneira mais simples é usar await Task.Run(() => task.Wait()), por exemplo. Tenha em mente que evitar tarefas de sincronização sobre assíncrono é fortemente recomendado, pois prejudica a escalabilidade e o desempenho da aplicação.

Resumo: Trabalhando 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.