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:

What should be the execution result of the following code? using System; using System.Threading.Tasks; using static System.Console; using static System.IO.File;...
In the previous post, I asked which function, in the following code, would fill the array with 1’s faster and...
A publicação original desse post ocorreu em meu blog, em 2011, e gerou uma bela discussão. Infelizmente, essa publicação não...
Nesse ano, palestrei na APIX sobre microsserviços. Abaixo, registro em vídeo feito pela organização do evento. Comentários? Feedback?
If you need to improve the performance of .NET applications, then at some point you will need to understand how...
Some days ago, I heard a fantastic interview with Phil Haack on the IT Career Energizer Podcast. Here is the...
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?