C#

Generalizando tipos de retornos assíncronos no C# 7.0

Aprenda a criar um tipo que pode ser retornado pelos métodos assíncronos do C#.

há 6 anos 1 mês

Formação Desenvolvedor C#
Conheça a formação em detalhes

Até a versão 6.0 do C#, os métodos assíncronos poderiam retornar os tipos Task, Task<T> e void.

Por se tratar de um objeto, em algumas situações os retornos de Task<T>, podem apresentar problemas de performance. Alocado na memória heap e coletado pelo garbage coletor, ao fazer uso intenso de métodos assíncronos, a performance de uma aplicação pode ser impactada pela volumosa alocação dos objetos Task<T>.

Para resolver este problema, na versão 7.0 do C#, foi adicionado o recurso que permite criar tipos de retorno para os métodos assíncronos.

C# (C Sharp) Básico
Curso C# (C Sharp) Básico
Conhecer o curso

Regras para a criação de um tipo Task<T>

Ao ler a documentação sobre os tipos de retorno assíncrono, é dito que qualquer tipo que contenha um método GetAwaiter público, que retorne um objeto que implemente a interface System.Runtime.CompilerServices.ICriticalNotifyCompletion, pode ser utilizado. Mas não é apenas isso.

Além deste detalhe, os tipos retornados por um método assíncrono, devem seguir um padrão “task type”. Pode ser uma classe ou estrutura, mas precisam estar associados a um builder type, identificado com o atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Caso retornem valores, deve ser definido como um tipo genérico.

O “tipo task” em si, só necessita implementar o método GetAwaiter público, mas o objeto retornado por este método, deve implementar a interface ICriticalNotifyCompletion, o método GetResult e possuir uma propriedade pública chamada IsCompleted.

Já o builder type a ser criado, deve corresponder a classe ou estrutura definida. Não se pode utilizar um existente e deve possuir a estrutura abaixo:

class MyTaskMethodBuilder<T>
{
    public static MyTaskMethodBuilder<T> Create();

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine;

    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;

    public MyTask<T> Task { get; }
}

Para tipos não genéricos, o método SetResult não possui parâmetro.

A classe acima será utilizada pelo compilador para transformar tipo assíncrono como um método assíncrono na compilação para o MSIL.

Você pode ver mais detalhes do funcionamento desta classe, aqui.

Parece complicado? Vamos a um exemplo para entender estes pontos.

Criando um tipo assíncrono/Task

Inicialmente criarei uma classe que implementará a interface ICriticalNotifyCompletion, que chamarei de MyTaskAwaiter:

public class MyTaskAwaiter<TResult> : ICriticalNotifyCompletion
{
    //Retorna se a operação foi concluída
    public bool IsCompleted => _value.IsCompleted;
    private readonly MyTask<TResult> _value;

    //Inicializa a classe com a classe Task criada
    public MyTaskAwaiter(MyTask<TResult> value)
    {
        this._value = value;
    }

    //Retorna o resultado
    public TResult GetResult() => _value.GetResult();

    //Define uma continuação.
    public void OnCompleted(Action continuation)
    {
        _value.AsTask().ConfigureAwait(continueOnCapturedContext: true).GetAwaiter().OnCompleted(continuation);
    }

    //Define uma continuação.
    public void UnsafeOnCompleted(Action continuation)
    {
        _value.AsTask().ConfigureAwait(continueOnCapturedContext: true).GetAwaiter().UnsafeOnCompleted(continuation);
    }
}

Os métodos mais importantes desta classe são, o GetResult e o OnCompleted, que, respectivamente, retorna o resultado da “Task”, e informa quando ela foi concluída.

Note que a “Task” é uma classe customizada, que será definida com o seguinte código:

[AsyncMethodBuilder(typeof(MyTaskBuilder<>))]
public class MyTask<TResult>
{
    //Define uma propriedade Task para indicar quando uma operação assincrona foi finalizada
    private Task<TResult> _task;

    //O resultado retornado pela classe
    private TResult _result;

    //Indica que a operação foi finalizada ou não
    public bool IsCompleted => _task == null || _task.IsCompleted;

    //Inicializando a classe com o resultado da operação bem sucedida
    public MyTask(TResult result){
        this._task = null;
        this._result = result;
    }

    //Obtém o "Awaiter" da classe
    public MyTaskAwaiter<TResult> GetAwaiter() => new MyTaskAwaiter<TResult>(this);

    //Retorna uma task, ou cria uma para o resultado
    public Task<TResult> AsTask() => _task ?? Task.FromResult(_result);

    //Retorna o resultado
    public TResult GetResult() =>
        _task == null ? 
            _result : 
            _task.GetAwaiter().GetResult();
}

Mesmo necessitando apenas de um método GetAwaiter, note que a classe definiu outros métodos e propriedades para facilitar o seu uso.

Por fim, é necessário definir o builder type para a classe acima, que deve ter o código abaixo:

public class MyTaskBuilder<TResult>
{
    private AsyncTaskMethodBuilder<TResult> _methodBuilder;
    private TResult _result;
    private bool _haveResult;
    private bool _useBuilder;

    //Cria o Buider Type
    public static MyTaskBuilder<TResult> Create() => new MyTaskBuilder<TResult>() { _methodBuilder = AsyncTaskMethodBuilder<TResult>.Create() };

    //Inicia o Buider Type
    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { _methodBuilder.Start(ref stateMachine); }

    //Define o State Machine para o Buider Type
    public void SetStateMachine(IAsyncStateMachine stateMachine) { _methodBuilder.SetStateMachine(stateMachine); }

    //Define o resultado
    public void SetResult(TResult result)
    {
        if (_useBuilder)
        {
            _methodBuilder.SetResult(result);
        }
        else
        {
            _result = result;
            _haveResult = true;
        }
    }

    //Gera uma exceção
    public void SetException(Exception exception) => _methodBuilder.SetException(exception);

    //Retorna a task vinculado ao builder type
    public MyTask<TResult> Task {
        get {
            if (_haveResult)
            {
                return new MyTask<TResult>(_result);
            }
            else
            {
                _useBuilder = true;
                return new MyTask<TResult>(_methodBuilder.Task.Result);
            }
        }
    }

    //Define o comportamento quando for necessário aguardar a conclusão da operação assincronona
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine 
    {
        _useBuilder = true;
        _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);
    }

    //Define o comportamento quando for necessário aguardar a conclusão da operação assincronona
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { 
        _useBuilder = true;
        _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
    }
}

Com a definição dessas três classes, podemos retornar o tipo MyTask em métodos assíncronos.

Utilizando o tipo assíncrono

O uso desta classe não difere do uso das classes Task ou Task em um método assíncrono:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Retorno: " + MyMethod().GetResult());
    }

    public static async MyTask<int> MyMethod(){
        var result = await MyMethodAsync();

        return result;
    }

    public static MyTask<int> MyMethodAsync()
    {
        return new MyTask<int>(5);
    }
}

Mesmo permitindo a criação de tipos assíncronos, criar um não é algo recomendado. Tudo que vimos acima só deve ser utilizado em casos específicos. Se o objetivo for apenas melhorar a performance da aplicação, basta substituir o tipo Task<T>, pelo ValueTask<T>.

ValueTask<T>

O tipo System.Threading.Tasks.ValueTask<T> foi adicionado na versão 7.0 do C#, como uma forma de superar os gargalos da classe Task<T>. Definido como uma estrutura, os objetos de ValueTask<T> não são salvos na memória heap, o que já contorna o problema com alocação de memória da classe Task<T>.

O seu uso é bem simples:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Retorno: " + MyMethod().Result);
    }

    public static async ValueTask<int> MyMethod(){
        var result = await MyMethodAsync();

        return result;
    }

    public static ValueTask<int> MyMethodAsync()
    {
        return new ValueTask<int>(5);
    }
}

E por não ser removida da memória pelo garbage collector, ela permite até a criação de “cache”:

class Program
{
    static void Main(string[] args)
    {
var task1 = CachedMethod();
        Console.WriteLine("Retorno da primeira chamada: " + task1.Result);

        var task2 = CachedMethod();
        Console.WriteLine("Retorno da segunda chamada: " + task2.Result);

        var task3 = CachedMethod();
        Console.WriteLine("Retorno da segunda chamada: " + task3.Result);
    }

    public static ValueTask<int> CachedMethod()
    {
        return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(LoadCache());
    }
    private static bool cache = false;
    private static int cacheResult;
    private static async Task<int> LoadCache()
    {
        // simulando um processamento assincrono:
        await Task.Delay(100);
        cacheResult = new Random().Next(5000, 10000);
        cache = true;
        return cacheResult;
    }
}

A execução do código acima será:

Resultado da execução do código

Conclusão

Como recomendado na documentação, no C# 7.0, procure sempre utilizar a classe ValueTask<T>, pois o seu uso já se provou ser mais performático que a classe Task<T>.

C# (C Sharp) Básico
Curso C# (C Sharp) Básico
Conhecer o curso

C# (C Sharp) Intermediário
Curso C# (C Sharp) Intermediário
Conhecer o curso

C# (C Sharp) Avançado
Curso C# (C Sharp) Avançado
Conhecer o curso

Autor(a) do artigo

Wladimilson M. Nascimento
Wladimilson M. Nascimento

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

Todos os artigos

Artigos relacionados Ver todos