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.
Curso C# (C Sharp) Básico
Conhecer o cursoRegras 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á:
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>.