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:

Não são raros os casos onde a distância entre um “desejo” e a “realização” é a “tentativa”. Ou seja, partindo...
Last post, I asked an explanation about the execution result of the following code. using System; using System.Threading.Tasks; using static...
07 de julho de 2016, aproximadamente 8:30 – Eu iria palestrar no TDC de São Paulo naquele dia. Aterrizamos em...
In this post, I would like to share my first attempt to create a (still) elementary search library. The source...
Write code is not a simple task. It is easy to make mistakes that result in bad performance. The last...
Nem todos os problemas podem ser resolvidos da mesma forma. Nem toda ferramenta é apropriada para todo tipo de trabalho....

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano anual do Clube de Estudos:

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano mensal do Clube de Estudos:

× Precisa de ajuda?