Partilhar via


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.StartNewou ) 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

  1. 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.

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

  3. 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.

  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. 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 para List<T>.ForEach(async element => ...) qualquer outro método que aceite um Action<T>, uma vez que o delegado assíncrono será coagido a um async 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). notIntendedTasknã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.Rune 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.