SOLUTION: What does this code do?

Last post, I asked an explanation about the execution result of the following code.

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;
        }
    }
}

Running it, we get this:

The code starts executing when I call the function, but I just get the exception when I ask the task result.

Explanation

C# language was designed to provide a good coding experience for programmers who needs to write asynchronous programs. There are no explicit continuations in the code. A great effort was made to make async method looks normal, but there is no free lunch.

Under the hood, the compiler does the best possible to translate async methods in a very efficient manner. The compiler generates the following code (you don’t need to know it to write async methods).

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;
}

As you can see, all the original implementation is gone. What we have is a code that creates an instance of a state machine, starting this state machine in the sequence.

Here is a similar implementation of the state machine that would be generated by the compiler.

[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 orchestrates the execution of this state machine. It’s pretty nice to see how exceptions are handled, right?!

An essential detail to observe is the execution status verification of the potentially asynchronous code (IsCompleted property) as soon as possible. The strategy adopted here gets the best performance when there is no reason to wait.

Also, a practical lesson to learn here is that Microsoft chooses mutable structs frequently, under the hood, to ensure the best performance (structs are used in the implementation of enumerators too). Structs are cheaper than classes (no heap, no GC).

How to get the exception sooner

Exceptions thrown by method arguments validation code should be considered when the method is called. This is not what happens in my original implementation.

The following code was proposed by Alberto Monteiro in the comments of the Portuguese version of the post that proposes the challenge and solves the problem.

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;
        }
    });
}

Here, there is no async modifier, so, the compiler will not touch the code.

Another alternative would be to have two methods, one public with verification, another private with the implementation.

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;
    }
}

We could have a local function as well.

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();
}

Conclusion

Microsoft did a great work making asynchronous code easier to write. But, sometimes, we have some undesirable side-effects. It’s important to know the basics of how the compiler helps us to avoid surprises.

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:

Mudança nunca é fácil. Em uma organização, então, este tema costuma ser algo bem complicado. A execução da estratégia implica...
Este exemplo é inspirado no livro do Ayende Se você deseja aprender RavenDB, recomendo que se inscreva no RavenDB bootcamp...
Nesse ano, palestrei na APIX sobre microsserviços. Abaixo, registro em vídeo feito pela organização do evento. Comentários? Feedback?
Nos últimos dois posts demonstrei como as alocações podem implicar em penalidades de performance. Nesse post, parto, novamente, de código...
Nessa semana, em uma dessas conversas que valem a pena, surgiu uma breve discussão sobre um texto antigo de Rubem...
The example which motivated this post comes from the excellent book Designing Distributed Systems by Brendan Burns (co-founder of the...
Masterclass

O Poder do Metamodelo para Profissionais Técnicos Avançarem

Nesta masterclass aberta ao público, vamos explorar como o Metamodelo para a Criação, desenvolvido por Elemar Júnior, pode ser uma ferramenta poderosa para alavancar sua carreira técnica em TI.

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?