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:

2 respostas

  1. Elemar, uma outra possível solução não seria apenas tornar o método Run() também assincrono e “awaitar” nele o método ComputeFileLengthAsync() ?
    Dessa forma a execução aguardaria o resultado e a exception seria exibida antes do “Computing file length”.

    [csharp]
    static async void Run()
    {
    var result = await ComputeFileLengthAsync(null);
    WriteLine("Computing file length");
    WriteLine(result);
    WriteLine("done!");
    }
    [/csharp]
    Valeu!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

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:

Se há algo que aprendi na “escola da vida” é que sempre que temos algo importante para decidir, se não...
Sempre fui péssimo jogando videogames. Aliás, esse é um dos motivos para eu ter começado a olhar computadores de outra...
Este é o primeiro post da série em que vou compartilhar algum conhecimento sobre como desenvolver uma aplicação de verdade...
No meu cotidiano, reconheço que, por mais estranho que pareça, comprometo muito do meu tempo ouvindo música ruim até que,...
When designing systems that need to scale you always need to remember that using better resources could help you to...
Publicado originalmente no meu blog em 2011 (infelizmente, este conteúdo não está mais disponível). Também publiquei no Linkedin. A publicação...