SOLUÇÃO: O que esse código faz?

No último post, solicitei uma explicação para o resultado da execução do código que segue:

using System;
using System.Threading.Tasks;
 
using static System.Console;
using static System.IO.File;
 
class Program
{
    static void Main(string[] args)
    {
        Run();
        ReadLine();
    }
    static void Run()
    {
        var task = ComputeFileLengthAsync(null);
        WriteLine("Computing file length");
        WriteLine(task.Result);
        WriteLine("done!");
    }
 
    static async Task<int> ComputeFileLengthAsync(string fileName)
    {
        WriteLine("Before If");
        if (fileName == null)
        {
            WriteLine("Inside");
            throw new ArgumentNullException(nameof(fileName));
        }
        WriteLine("After");
 
        using (var fileStream = OpenText(fileName))
        {
            var content = await fileStream.ReadToEndAsync();
            return content.Length;
        }
    }
}

Eis o resultado da execução:

Tenho execução normal, mas somente recebo a exceção quando solicito o resultado da task.

Explicação

C# foi projetada para proporcionar uma boa experiência de codificação para aqueles que precisam escrever código assíncrono. Não há continuations explícitas no código. Um grande esforço foi feito para fazer com que métodos marcados com async parecessem normais, mas nada é de graça.

Por baixo do capô, o compilador faz o melhor possível para traduzir métodos marcados com async de forma eficiente. O código que segue seria gerado pelo compilador.

static Task<int> ComputeFileLengthAsync(string fileName)
{
    var sm = new ComputeFileLengthAsync_StateMachine
    {
        fileName = fileName,
        _builder = AsyncTaskMethodBuilder<int>.Create(),
        _state = -1
    };

    sm._builder.Start(ref sm);
    return sm._builder.Task;
}

Como você pode ver, o código original não está aqui. O que temos é um código que cria uma instância de uma máquina de estados e inicia sua execução.

Aqui, uma implementação similar a gerada pelo compilador para a máquina de estados.

[StructLayout(LayoutKind.Auto)]
public struct ComputeFileLengthAsync_StateMachine : IAsyncStateMachine
{
    public int _state;
    public AsyncTaskMethodBuilder<int> _builder;
    public string fileName;
    private StreamReader _fileStream;
    private TaskAwaiter<string> _awaiter;

    public void MoveNext()
    {
        int num = _state;
        int length;
        try
        {
            if (num != 0)
            {
                Console.WriteLine("Before If");
                if (fileName == null)
                {
                    Console.WriteLine("Inside");
                    throw new ArgumentNullException("fileName");
                }
                Console.WriteLine("After");
                _fileStream = File.OpenText(fileName);
            }
            try
            {
                TaskAwaiter<string> taskAwaiter;
                if (num != 0)
                {
                    taskAwaiter = _fileStream.ReadToEndAsync().GetAwaiter();
                    if (!taskAwaiter.IsCompleted)
                    {
                        num = (_state = 0);
                        _awaiter = taskAwaiter;
                        _builder.AwaitUnsafeOnCompleted(ref taskAwaiter, ref this);
                        return;
                    }
                }
                else
                {
                    taskAwaiter = _awaiter;
                    _awaiter = default(TaskAwaiter<string>);
                    num = (_state = -1);
                }
                string result = taskAwaiter.GetResult();
                length = result.Length;
            }
            finally
            {
                if (num > 0)
                {
                    _fileStream?.Dispose();
                }
            }
        }
        catch (Exception exception)
        {
            _state = -2;
            _builder.SetException(exception);
            return;
        }
        _state = -2;
        _builder.SetResult(length);
    }

    [DebuggerHidden]
    public void SetStateMachine(IAsyncStateMachine stateMachine)
    {
        _builder.SetStateMachine(stateMachine);
    }
}

AsyncTaskMethodBuilder coordena a execução da máquina de estados. É interessante observar como exceções são tratadas.

Um detalhe essencial por observar é q verificação do status de execução do código potencialmente assíncrono (através da propriedade IsCompleted), o mais cedo possível. Esta estratégia garante a melhor performance quando não há o que esperar.

Também é interessante aprender o uso que a Microsoft faz de structs mutáveis para garantir a melhor performance (structs mutáveis também são usadas na implementação de enumerators). Structs são mais baratas computacionalmente que classes (não há uso do Heap e não há impacto para o GC).

Como receber a exceção mais cedo

Exceptions disparadas na verificação de argumentos deveriam ser tratadas quando o método é chamado. Isso não é o que ocorre na minha implementação original.

O código que segue foi proposto pelo Alberto Monteiro nos comentários do post anterior e resolve o problema.

static Task<int> ComputeFileLengthAsync(string fileName)
{
    Console.WriteLine("Before If");
    if (fileName == null)
    {
        Console.WriteLine("Inside");
        throw new ArgumentNullException("fileName");
    }
    Console.WriteLine("After");

    return new Task<int>(() =>
    {
        using (var fileStream2 = OpenText(fileName))
        {
            var content2 = fileStream2.ReadToEndAsync().Result;
            return content2.Length;
        }
    });
}

Aqui, não há uso do moficador async, então o compilador não irá mexer nesse código.

Outra alternativa é ter dois métodos. Um público que faz a verificação e outro privado apenas com a implementação.

public static Task<int> ComputeFileLengthAsync(string fileName)
{
    Console.WriteLine("Before If");
    if (fileName == null)
    {
        Console.WriteLine("Inside");
        throw new ArgumentNullException("fileName");
    }
    Console.WriteLine("After");

    return ComputeFileLengthAsyncImpl(fileName);
}

private static async Task<int> ComputeFileLengthAsyncImpl(string fileName)
{
    using (var fileStream2 = OpenText(fileName))
    {
        var content2 = await fileStream2.ReadToEndAsync();
        return content2.Length;
    }
}

Poderíamos ter uma função localtambém..

public static Task<int> ComputeFileLengthAsync(string fileName)
{
    Console.WriteLine("Before If");
    if (fileName == null)
    {
        Console.WriteLine("Inside");
        throw new ArgumentNullException("fileName");
    }
    Console.WriteLine("After");

    async Task<int> ComputeFileLengthAsyncImpl()
    {
        using (var fileStream2 = OpenText(fileName))
        {
            var content2 = await fileStream2.ReadToEndAsync();
            return content2.Length;
        }
    }

    return ComputeFileLengthAsyncImpl();
}

Conclusão

Microsoft fez um grande trabalho tornando código assíncrono mais fácil de escrever. Entretanto, algumas vezes, temos alguns efeitos colaterais indesejados. É importante entender o básico de como o compilador nos ajuda para evitar surpresas.

Compartilhe este insight:

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mais insights para o seu negócio

Veja mais alguns estudos e reflexões que podem gerar alguns insights para o seu negócio:

In this post, I will share how to write an ASP.NET Core Identity Storage Provider from the Scratch using RavenDB....
Entropia é um conceito oriundo da física que trata da quantidade de “desordem” em um sistema. As leis da termodinâmica...
Na Guiando, a área de Implantação também está adotando Kanban (Não ficamos restritos ao desenvolvimento). Começando por lá, resolvemos adotar...
Mais uma vez, tive a honra de participar de um excelente bate-papo sobre microsserviços com o pessoal da Lambda 3....
In a previous post, I wrote about how to support sagas using a Workflow Engine as Saga Execution Coordinator (if...
Anualmente, como Microsoft MVP & RD, participo de uma conferência global, organizada pela Microsoft, na sede da empresa em Redmond....
Oferta de pré-venda!

Mentoria em
Arquitetura de Software

Práticas, padrões & técnicas para Arquitetura de Software, de maneira efetiva, com base em cenários reais para profissionais envolvidos no projeto e implantação de software.

× Precisa de ajuda?