No último post, solicitei uma explicação para o resultado da execução do código que segue:
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;
}
}
}
Eis o resultado da execução:
Tenho execução normal, mas somente recebo a exceção quando solicito o resultado da task.
Explicação
C# foi projetada para proporcionar uma boa experiência de codificação para aqueles que precisam escrever código assíncrono. Não há continuations explícitas no código. Um grande esforço foi feito para fazer com que métodos marcados com async parecessem normais, mas nada é de graça.
Por baixo do capô, o compilador faz o melhor possível para traduzir métodos marcados com async de forma eficiente. O código que segue seria gerado pelo compilador.
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;
}
Como você pode ver, o código original não está aqui. O que temos é um código que cria uma instância de uma máquina de estados e inicia sua execução.
Aqui, uma implementação similar a gerada pelo compilador para a máquina de estados.
[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 coordena a execução da máquina de estados. É interessante observar como exceções são tratadas.
Um detalhe essencial por observar é q verificação do status de execução do código potencialmente assíncrono (através da propriedade IsCompleted), o mais cedo possível. Esta estratégia garante a melhor performance quando não há o que esperar.
Também é interessante aprender o uso que a Microsoft faz de structs mutáveis para garantir a melhor performance (structs mutáveis também são usadas na implementação de enumerators). Structs são mais baratas computacionalmente que classes (não há uso do Heap e não há impacto para o GC).
Como receber a exceção mais cedo
Exceptions disparadas na verificação de argumentos deveriam ser tratadas quando o método é chamado. Isso não é o que ocorre na minha implementação original.
O código que segue foi proposto pelo Alberto Monteiro nos comentários do post anterior e resolve o problema.
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;
}
});
}
Aqui, não há uso do moficador async, então o compilador não irá mexer nesse código.
Outra alternativa é ter dois métodos. Um público que faz a verificação e outro privado apenas com a implementação.
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;
}
}
Poderíamos ter uma função localtambém..
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();
}
Conclusão
Microsoft fez um grande trabalho tornando código assíncrono mais fácil de escrever. Entretanto, algumas vezes, temos alguns efeitos colaterais indesejados. É importante entender o básico de como o compilador nos ajuda para evitar surpresas.