DEV Community

Cover image for Async/Await: O que tem de novo no .NET 8?
Angelo Belchior
Angelo Belchior

Posted on • Edited on

Async/Await: O que tem de novo no .NET 8?

Habemus .NET 8!

Esse lançamento traz uma tonelada de novidades. Recomendo muito que você leia esse blog post para ter noção de tudo que foi entregue nesse release.

Dentre centenas de novidades, separei uma interessante referente a feature async/await envolvendo do método ConfigureAwait.

Porém, antes de continuar recomendo muito a leitura desse post Async/Await: Task.ConfigureAwait, Deadlock e Pink Floyd.
Nele eu destrincho o funcionamento do método ConfigureAwait, passando por cenários onde podemos ter sérios problemas de lock e explico como podemos evitar isso.

Dito isto, vamos em frente!

No .NET 7, o método ConfigureAwait recebia um parâmetro booleano para indicar se a task voltaria para o contexto original ou não.

/// Configura um aguardador para aguardar a Task  
/// continueOnCapturedContext:   
/// true para tentar organizar a continuação de volta ao contexto original capturado; caso contrário, falso.  
/// returns: Um objeto usado para aguardar a tarefa.
public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext)  
{  
    return new ConfiguredTaskAwaitable<TResult>(this, continueOnCapturedContext);
}
Enter fullscreen mode Exit fullscreen mode

Já no .NET 8 foi criada uma sobrecarga para esse método onde podemos passar um enum chamado ConfigureAwaitOptions.

/// Configura um aguardador para aguardar a Task 
/// options: Opções usadas para configurar como a espera nesta tarefa vai ser executada.
/// returns: Um objeto usado para aguardar a tarefa.
public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitOptions options)
{
    if ((options & ~(ConfigureAwaitOptions.ContinueOnCapturedContext |
                     ConfigureAwaitOptions.ForceYielding)) != 0)
    {
        ThrowForInvalidOptions(options);
    }

    return new ConfiguredTaskAwaitable<TResult>(this, options);

    static void ThrowForInvalidOptions(ConfigureAwaitOptions options) =>
        throw ((options & ConfigureAwaitOptions.SuppressThrowing) == 0 ?
            new ArgumentOutOfRangeException(nameof(options)) :
            new ArgumentOutOfRangeException(nameof(options), SR.TaskT_ConfigureAwait_InvalidOptions));
}
Enter fullscreen mode Exit fullscreen mode

E quais seriam as opções do ConfigureAwaitOptions? Vejamos o código fonte do enum:

/// Opções para controlar o comportamento durante a espera.
[Flags]
public enum ConfigureAwaitOptions
{
    None = 0x0,
    ContinueOnCapturedContext = 0x1,
    SuppressThrowing = 0x2,
    ForceYielding = 0x4,
}
Enter fullscreen mode Exit fullscreen mode
  • None: Nenhuma opção especificada. É o mesmo que usar Task.ConfigureAwait(false).
  • ContinueOnCapturedContext: Tenta empacotar a continuação de volta ao SynchronizationContext original ou presente na thread de origem no momento da espera. É o mesmo que usar Task.ConfigureAwait(true).
  • SuppressThrowing: Evita lançar uma exceção na conclusão da espera de uma tarefa que termina com o estado TaskStatus.Faulted ou TaskStatus.Canceled.
  • ForceYielding: Força uma espera em uma tarefa já concluída a se comportar como se a tarefa ainda não tivesse sido concluída, de modo que o método assíncrono atual será forçado a produzir sua execução.

Um ponto importante: Esse enum aceita bitwise combination, logo podemos ter a combinação de valores, como por exemplo.

task.ConfigureAwait(ConfigureAwaitOptions.ForceYielding | 
                    ConfigureAwaitOptions.None);
Enter fullscreen mode Exit fullscreen mode

Bem, as duas primeiras opções, None e ContinueOnCapturedContext foram detalhadas nesse post conforme eu disse acima.

Já o SuppressThrowing é bem interessante, e acredito que ele vai ser bem útil, afinal, tem certas ocasiões que eu não gostaria de receber uma exception caso a task seja cancelada por um CancellationToken por exemplo. Aliás, para saber mais sobre o CancellationToken, tem um post bacanudo pra você: Async/Await: Para que serve o CancellationToken?

Um exemplo simples:

try
{
    var cancellationTokenSource = new CancellationTokenSource(10);
    var cancellationToken = cancellationTokenSource.Token;
    await Task.Delay(10000, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); 
    Console.WriteLine("Finalizou o processo...");
}
catch(Exception ex)
{
    Console.WriteLine(ex.Message);
}

Console.ReadLine();
Enter fullscreen mode Exit fullscreen mode

Nesse caso acima, passamos ConfigureAwaitOptions.None como argumento ao ConfigureAwait, sendo assim, passados 10 milisegundos, o CancellationToken notifica o método Delay e uma exceção é lançada: "A task was canceled."
Caso troquemos o parâmetro por ConfigureAwaitOptions.SuppressThrowing, o processo é interrompido, mas não lança uma exceção.. Sendo assim, receberemos a mensagem: "Finalizou o processo....". Interessante, não?

Já o ConfigureAwaitOptions.ForceYielding é semelhante a Task.Yield. Logo, para explicar um, devo explicar o outro.

Vamos começar com o Task.Yield.

O Task.Yield é um método que é usado para criar uma pausa em uma execução assíncrona, permitindo que outras tasks na mesma fila de contexto assíncrono tenham a chance de serem executadas. Esse método é muitas vezes utilizado em cenários onde você quer liberar o controle temporariamente para o sistema de execução assíncrona, permitindo que ele execute outras tarefas agendadas.

Ficou confuso? Aqui vai um exemplo simples :)

var task1 = MinhaTarefaAssincrona("Tarefa 1");
var task2 = MinhaTarefaAssincrona("Tarefa 2");

await Task.WhenAll(task1, task2);

return;

static async Task MinhaTarefaAssincrona(string nome)
{
    Console.WriteLine($"Início da {nome}");

    // Simula uma tarefa assíncrona demorada
    await Task.Delay(1000);

    await Task.Yield();

    Console.WriteLine($"Fim da {nome}");
}
Enter fullscreen mode Exit fullscreen mode

Explicando o código:

Neste exemplo, o programa inicia duas tarefas assíncronas simultaneamente (MinhaTarefaAssincrona("Tarefa 1") e MinhaTarefaAssincrona("Tarefa 2")). Cada uma dessas tarefas usa Task.Yield para criar uma pausa durante a execução, permitindo que outras tarefas na mesma fila de contexto assíncrono tenham a oportunidade de serem executadas.

O método Task.WhenAll é usado para esperar que ambas as tarefas sejam concluídas.

E como sabemos que essa pausa de uma thread ocorreu?

Vamos olhar a saída da execução.

/Volumes/SSD/Git/ConfigureAwait/ConfigureAwait/bin/Debug/net8.0/ConfigureAwait 
Início da Tarefa 1
Início da Tarefa 2
Fim da Tarefa 2
Fim da Tarefa 1
Enter fullscreen mode Exit fullscreen mode

Agora, caso a gente remova o await Task.Yield(); a saída muda:

/Volumes/SSD/Git/ConfigureAwait/ConfigureAwait/bin/Debug/net8.0/ConfigureAwait 
Início da Tarefa 1
Início da Tarefa 2
Fim da Tarefa 1
Fim da Tarefa 2
Enter fullscreen mode Exit fullscreen mode

O que podemos notar avaliando essas saídas é que, quando temos o await Task.Yield(); Tarefa 1 é pausada e aguarda a Tarefa 2 ser finalizada. Removendo o await Task.Yield(); temos a sequência de execução natural, onde a Tarefa 1 é a primeira a começar a ser executada e é a primeira a finalizar. Em seguida temos a Tarefa 2 que é a segunda a ser executada e é a última a finalizar.

Isso demonstra como Task.Yield pode ser usado para melhorar a concorrência e a eficiência em certos cenários assíncronos.

Já o ConfigureAwaitOptions.ForceYielding tem um comportamento bem parecido, porém com uma opção a mais: como podemos combinar valores, podemos escolher em qual contexto o retorno da pausa vai ser executado. Seria algo como...

task.ConfigureAwait(ConfigureAwaitOptions.ForceYielding | 
                    ConfigureAwaitOptions.ContinueOnCapturedContext);
Enter fullscreen mode Exit fullscreen mode

Mais informações, acesse a documentação Task.ConfigureAwait Method (System.Threading.Tasks) | Microsoft Learn

Basicamente é isso :)

Espero que tenham gostado e até a próxima \o/

Top comments (1)

Collapse
 
yogini16 profile image
yogini16

Thank you for sharing !!