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:

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:

Em setembro do ano passado, resolvi assinar a versão digital de um jornal, especializado em economia, de grande circulação no...
I wrote this post in 2016. Unfortunately, I lost it when I “rebooted” my blog. Anyway, I have a good...
Uma das causas mais comuns para problemas de performance em .NET é o descuido com o Garbage Collector. Mesmo funções...
Há pouco mais de um ano, assumi o compromisso de ajudar, como CTO, a Guiando a escalar seu negócio e...
Agora em fevereiro, depois de 18 meses, fechei meu ciclo como CTO na Guiando para integrar o conselho consultivo da...
Há quase um mês, resolvi intensificar a comunicação da EximiaCo, dessa vez, em um canal dedicado ao público técnico, no...