terça-feira, 29 de março de 2011

Prepare-se para o C# 5 – Parte 2

Demorou mais chegou... rs

No último artigo mostramos que a expressão document = await Fetchasync(urls [i]) do C# 5.0 era realizada assim:

state = State.AfterFetch;
fetchThingy = FetchAsync(urls[i]);
if (fetchThingy.SetContinuation(archiveDocuments))
  return;
AfterFetch: ;
  document = fetchThingy.GetValue();

Nesse modelo, um método assíncrono retorna um Task<T>, e por hora, vamos assumir que o FetchAsync também retorne um Task<Document>. Então, o código atual será realizado assim:

fetchAwaiter = FetchAsync(urls[i]).GetAwaiter();
state = State.AfterFetch;
if (fetchAwaiter.BeginAwait(archiveDocuments))
  return;
AfterFetch: ;
  document = fetchAwaiter.EndAwait();

A chamada a FetchAsync cria e retorna um Task<Document>. Ao chamar este método ele imediatamente retorna um Task<Document> que de alguma forma busca o documento desejado de forma assíncrona. Para fazer alguma coisa quando ele estiver completo buscamos na tarefa por um Awaiter, e ele fornece 2 métodos: BeginAwait, assina a continuação para a tarefa (executado após a finalizada a tarefa) e o EndAwait, que extrai o resultado da tarefa finalizada.

Como esses métodos são implementados na Task (para métodos void) ou Task<T> (para métodos que retornem valor) como fica os métodos que não retornam Task ou Task<T>?

Nesse caso é utilizado a mesma estratégia do LINQ, ou seja, se tivermos:

From c in customers where c.City == "London"

Isso é traduzido para:

Customers.Where(c => c.City == "London")

E uma resolução de overload tenta encontrar o melhor método "Where" checando se a implementação de "Customers" implementa tal método, caso contrário, busca por métodos de extensão.

Com o padrão GetAwaiter/BeginAwait/EndAwait é a mesma coisa, é feita a resolução de overload na transformação da expressão, verificado o método ou a extensão adequeada.

O insight é que assincronia não requer paralelismo, mas o paralelismo exige assincronia, e muitas das ferramentas úteis para o paralelismo podem ser usadas para assincronia sem paralelismo.

Por isso o uso do Task, ele não possui paralelismo inerente e podemos representar unidades de trabalho pendentes que pode ser paralelizada e não requer multithreading, além de, já possui mecanismos de cancelamento entre outras características úteis da TPL (Task Parallel Library).

Voltando ao exemplo da busca de documentos, ele foi deliberadamente planejado para demostrar o pulo do gato, o CPS (Continuation Passing Style), para controlar até mesmo orquestrações simples de 2 tarefas assíncronas de métodos void.

Então, vamos falar um pouco sobre composição de métodos assíncronos.

Suponha que o método ArchiveDocuments retornasse o número total de bytes arquivados:

long ArchiveDocuments(List<Url> urls)
{
  long count = 0;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = Fetch(urls[i]);
    count += document.Length;
    Archive(document);
  }
  return count;
}

Agora, vamos reescrevê-lo de forma assíncrona usando um CPS:

void ArchiveDocumentsAsync(List<Url> urls, Action<long> continuation)
{
  // de alguma forma executa a busca de forma assíncrona,
  // depois invoca sua continuação
}

Dessa forma, o chamador do ArchiveDocumentsAsync precisaria ser escrito em um CPS de modo que a continuidade possa ser passada. E se o retorno fosse um resultado? Então, seria uma confusão.

No modelo TAP (Task Asynchrony Pattern, nome provisório para essa feature de composição de métodos assíncronos), ao invés disso, diriamos que o tipo que representa o trabalho assíncrono e retorna um valor é um Task<T>. Em C# 5, você poderia simplesmente escrever:

async Task<long> ArchiveDocumentsAsync(List<Url> urls)
{
  long count = 0;
  Task archive = null;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = await FetchAsync(urls[i]);
    count += document.Length;
    if (archive != null)
      await archive;
    archive = ArchiveAsync(document);
  }
  return count;
}

E o compilador se encarregará de reescrever e gerar algo como:

Task<long> ArchiveDocuments(List<Url> urls)
{
  var taskBuilder = AsyncMethodBuilder<long>.Create();
  State state = State.Start;
  TaskAwaiter<Document> fetchAwaiter = null;
  TaskAwaiter archiveAwaiter = null;
  int i;
  long count = 0;
  Task archive = null;
  Document document;
  Action archiveDocuments = () =>
  {
    switch(state)
    {
      case State.Start: goto Start;
      case State.AfterFetch: goto AfterFetch;
      case State.AfterArchive: goto AfterArchive;
    }
    Start:
    for(i = 0; i < urls.Count; ++i)
    {
      fetchAwaiter = FetchAsync(urls[i]).GetAwaiter();
      state = State.AfterFetch;
      if (fetchAwaiter.BeginAwait(archiveDocuments))
        return;
      AfterFetch:
      document = fetchAwaiter.EndAwait();
      count += document.Length;
      if (archive != null)
      {
        archiveAwaiter = archive.GetAwaiter();
        state = State.AfterArchive;
        if (archiveAwaiter.BeginAwait(archiveDocuments))
          return;
        AfterArchive:
        archiveAwaiter.EndAwait();
      }
      archive = ArchiveAsync(document);
    }
    taskBuilder.SetResult(count);
    return;
  };
  archiveDocuments();
  return taskBuilder.Task;
}

Vamos fazer um teste de mesa aqui para clarear as coisas.

O que acontece quando a lista está vazia? Nós criamos um contrutor de tarefas, um delegate que retorna void e invocamos ele de forma assíncrona. Ele inicializa a variável ”count” com 0, executa a label start, pula o loop, diz para o helper “você tem um resultado” e retorna. Finaliza o delegate. O taskbuilder é solicitado para uma tarefa, e uma vez que ele sabe que a tarefa foi concluida, retorna a tarefa concluida que simplesmente representa o número 0.

E caso existam vários documentos de arquivos? Novamente criamos um taskbuilder e um delegate que serão invocados assíncronamente. No primeiro loop começamos uma busca assíncrona, assinamos o delegate como sua continuação e retornamos do delegate. O taskbuilder contrói uma tarefa que representa “estou trabalhando de forma assíncrona no corpo do ArchiveDocumentsAsync” e retorna essa tarefa. Ao ser concluída, invoca sua continuação e o delegate inicia de novo “do ponto onde ele parou” graças a máquina de estado. Tudo continua exatamente como antes, porém, direferente da versão void, o Task<long> retornado para o ArchiveDocumentsAsync sinaliza que finalizou (invocando sua continuação) e o delegate diz ao task builder para definir o resultado.

NOTA:Tal qual o LINQ, o TAP pode ser extensível por qualquer tipo que tenha um GetAwaiter, que retorne um tipo que tenha BeginAwait, EndAwait e assim por diante, para que possa ser usado nas expressões “await”. Contudo, métodos marcados para serem assíncronos devem retornar void, Task ou Task<T>.

Existem situações onde sintaxe com tokens como “where” do LINQ são mais naturais e outras onde são a sintaxe “fluent” .Where(c => ...), no TAP haverá métodos com nomes como WhenAll ou WhenAny para compor e orquestrar tarefas assíncronas:

List<List<Url>> groupsOfUrls = whatever;
Task<long[]> allResults = Task.WhenAll(
      from urls in groupsOfUrls
      select ArchiveDocumentsAsync(urls));
long[] results = await allResults;

O que isto faz? Bem, o ArchiveDocumentsAsync retorna um Task<long>, então a query retorna um IEnumerable<Task<long>>. WhenAll pega a sequência de tarefas e produz uma nova tarefa, que de forma assíncrona aguarda cada um deles, preenche um array com o resultado e invoca sua continuação com o resultado quanto ele estiver disponível.

Da mesma forma o WhenAny pega uma sequência de tarefas e cria uma tarefa que invoca sua continuação com o primeiro resultado quando qualquer uma das tarefas estiverem finalizadas.

Existirão outras combinações de tarefas e helpers relacionados. Veja mais exemplos no CTP e repare que como não é possível modificar um Task existente os combinadores foram provisóriamente adicionados na classe TaskEx,mas, serão movidos para o Task até a versão final.

Até a próxima!


Artigos Recomendados:

>>   Prepare-se para o C# 5 – Parte 1
>>   Reflection de Alta de Performance
>>   Extension Methods = Manutenibilidade


Nenhum comentário:

Postar um comentário