A postagem recente do blog Mantendo os aplicativos rápidos e fluidos com a assincronia no Tempo de Execução do Windows inclui exemplos de como a palavra-chave await no C# e no Visual Basic permite aos desenvolvedores usar operações assíncronas do WinRT e ao mesmo tempo manter um bom fluxo de controle.

Nesta postagem, continuaremos a tratar do assunto. Iremos mais fundo para entender exatamente como o await funciona com o WinRT. Este conhecimento facilitará o trabalho com códigos que usam await e, como consequência, o permitirá escrever melhor os aplicativos estilo Metro.

Para começar, vamos ver como é um mundo sem await.

Revisando os fundamentos

Toda assincronia no WinRT está enraizada em uma única interface: IAsyncInfo.

public interface IAsyncInfo
{
AsyncStatus Status { get; }
HResult ErrorCode { get; }
uint Id { get; }

void Cancel();
void Close();
}

Toda operação assíncrona no WinRT implementa essa interface, o que fornece a funcionalidade básica necessária para se dirigir a uma operação assíncrona e obter informações sobre sua identidade, status e também solicitar seu cancelamento. Mas essa interface específica deixa de apresentar o que provavelmente seria o aspecto mais importante de uma operação assíncrona: uma chamada de retorno para notificar um ouvinte quando a operação foi concluída. Esse recurso é propositalmente separado em quatro outras interfaces que solicitam IAsyncInfo. Toda operação assíncrona no WinRT implementa uma destas quatro interfaces:

public interface IAsyncAction : IAsyncInfo
{
AsyncActionCompletedHandler Completed { get; set; }
void GetResults();
}

public interface IAsyncOperation<TResult> : IAsyncInfo
{
AsyncOperationCompletedHandler<TResult> Completed { get; set; }
TResult GetResults();
}

public interface IAsyncActionWithProgress<TProgress> : IAsyncInfo
{
AsyncActionWithProgressCompletedHandler<TProgress> Completed { get; set; }
AsyncActionProgressHandler<TProgress> Progress { get; set; }
void GetResults();
}

public interface IAsyncOperationWithProgress<TResult, TProgress> : IAsyncInfo
{
AsyncOperationWithProgressCompletedHandler<TResult, TProgress> Completed { get; set; }
AsyncOperationProgressHandler<TResult, TProgress> Progress { get; set; }
TResult GetResults();
}

Essas quatro interfaces suportam todas as combinações com e sem geração de resultados, e também com e sem relatórios de andamento. Todas as interfaces expõem uma propriedade Completed, que pode ser definida em um delegado a ser executada quando a operação é concluída. Você talvez queira definir um delegado apenas uma vez. Caso o delegado seja definido após a conclusão da operação, ele será imediatamente agendado ou executado com a implementação controlando a concorrência entre a conclusão da operação e a atribuição do delegado.

Agora, digamos que você queira implementar um aplicativo estilo Metro com um Botão XAML de maneira que ao clicar no botão forma-se uma fila de trabalho na thread pool do WinRT para desempenhar uma intensa operação computacional. Quando o trabalho é concluído, o conteúdo do botão é atualizado com o resultado da operação.

Como podemos implementar isso? A classe ThreadPool do WinRT expõe um método para se executar o trabalho de maneira assíncrona no pool:

public static IAsyncAction RunAsync(WorkItemHandler handler);

Podemos usar esse método para enfileirar nosso intenso trabalho computacional evitando o bloqueio de nossa thread da interface do usuário durante sua execução:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
}

Descarregamos agora com sucesso o trabalho de uma thread da interface do usuário para o pool, mas como sabemos quando o trabalho está concluído? O RunAsync retorna um IAsyncAction para que usemos um manipulador de conclusão, receber aquela notificação e executar nossa continuação em resposta:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
btnDoWork.Content = result.ToString(); // bug!
};
}

Quando a operação assíncrona enfileirada na ThreadPool for concluída, nosso manipulador Completed será executado e tentará armazenar o resultado em nosso botão. Infelizmente, isso está quebrado no momento. O manipulador Completed dificilmente será executado em uma thread da interface do usuário e, ainda assim, para modificar btnDoWork.Content, o manipulador precisa ser executado na thread da interface do usuário (do contrário, o resultado será uma exceção com o código de erro RPC_E_WRONG_THREAD). Para lidar com isso, podemos usar o objeto CoreDispatcher associado a nossa interface de usuário para realizar marshaling da execução no lugar onde precisamos que ela esteja:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
btnDoWork.Content = result.ToString();
});
};
}

Isso já está em funcionamento. Mas e se o método Compute emitir uma exceção? E se alguém acionar Cancel no IAsyncAction retornado de ThreadPool.RunAsync? Nosso manipulador Completed precisará lidar com o fato de que IAsyncAction pode terminar em um de três estados terminais: Completed, Error ou Canceled:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); })
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
switch (asyncStatus)
{
case AsyncStatus.Completed:
btnDoWork.Content = result.ToString();
break;
case AsyncStatus.Error:
btnDoWork.Content = asyncAction.ErrorCode.Message;
break;
case AsyncStatus.Canceled:
btnDoWork.Content = "A task was canceled";
break;
}
});
};
}

Isso é uma boa quantidade de código a ser escrito para manipular uma única execução assíncrona; imagine como seria se precisássemos realizar muitas operações assíncronas em sequência? Não seria interessante se pudéssemos escrever códigos como este?

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}

Esse código se comporta exatamente como o código anterior. Mas não precisamos lidar manualmente com chamadas de retorno de conclusão. Não precisamos realizar marshaling manualmente para a thread da interface do usuário. Não precisamos verificar explicitamente status de conclusão. E não precisamos inverter nosso fluxo de controle, o que significa que agora é fácil estender para mais operações. Por exemplo, para realizar múltiplos processos computacionais e atualizações da interface do usuário em um loop:

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
for(int i=0; i<5; i++)
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();

}
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}

Pense por alguns instantes no código que você teria de escrever para alcançar isso usando IAsyncAction manualmente. Essa é a mágica da nova palavra-chave async/await no C# e no Visual Basic. A boa notícia é que você pode escrever exatamente esse código, e isso não é mágica. Ao longo desta postagem, vamos explorar como isso funciona nos bastidores.

Transformações de compilador

Marcar um método com a palavra-chave async faz com que o C# ou o compilador do Visual Basic reescreva a implementação do método usando uma máquina de estado. Usando essa máquina de estado, o compilador pode inserir pontos no método que pode suspender e continuar sua execução sem bloquear uma thread. Esses pontos não são inseridos ao acaso. Eles são inseridos apenas onde você explicitamente usa a palavra-chave await:

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await something; // <-- potential method suspension point
...
}

Enquanto você espera uma operação assíncrona que ainda não foi concluída, o código gerado pelo compilador garante que todos os estados associados ao método (por exemplo, variáveis locais) sejam empacotados e preservados no heap. Então, a função retorna ao chamador, permitindo que a thread na qual ela estava sendo executada faça outros trabalhos. Quando a operação assíncrona esperada é concluída mais tarde, o método de execução continua a usar o estado preservado.

Todo tipo que expõe o padrão de espera pode ser esperado. O padrão consiste em primeiramente expor um método GetAwaiter que retorna um tipo que fornece os membros IsCompleted, OnCompleted e GetResult. Quando você escreve:

await something;

o compilador gera um código que usa esses membros na instância para verificar se o objeto já foi concluído (via IsCompleted) e, caso não tenha concluído, para conectar uma continuação (via OnCompleted) que retorna uma chamada para continuar uma execução quando a tarefa é concluída. Após a conclusão da operação, qualquer exceção da operação é propagada e/ou seu resultado é retornado (via GetResult). Então, quando você escrever isto:

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await ThreadPool.RunAsync(delegate { result = Compute(); });
...
}

o compilador traduz isso em um código semelhante a isto:

private class btnDoWork_ClickStateMachine : IAsyncStateMachine
{
// Member fields for preserving locals and other necessary state
int $state;
TaskAwaiter<string> $awaiter;
int result;
...
// Method that moves to the next state in the state machine
public void MoveNext()
{
// Jump table to get back to the right statement upon resumption
switch (this.$state)
{
...
case 2: goto Label2;
...
}
...
// Expansion of await ...;
var tmp1 = ThreadPool.RunAsync(delegate { this.result = Compute(); });
this.$awaiter = tmp1.GetAwaiter();
if (!this.$awaiter.IsCompleted)
{
this.$state = 2;
this.$awaiter.OnCompleted(MoveNext);
return;
Label2:
}
this.$awaiter.GetResult();
...
}
}

Para um método btnDoWork_Click, o compilador gera uma classe de máquina de estado que contém um método MoveNext. Toda chamada para MoveNext continua a execução do método btnDoWork_Click até alcançar a próxima espera de algo que ainda não foi concluído, ou até o término do método, o que vier primeiro. Quando o código gerado por compilação localizar uma instância esperada que ainda não foi concluída, ele marcará o local atual com uma variável do estado, agendará a execução do método para continuar quando a instância esperada concluir e, então, retornará. Quando a instância esperada concluir, o método MoveNext será executado novamente e pulará para o ponto no método de onde a execução havia parado.

O compilador não se importa se na verdade IAsyncAction estiver sendo esperado aqui. Tudo o que importa para ele é que o padrão correto esteja disponível para associação. Claro, você já viu a aparência da interface IAsyncAction e também já viu que ela não contém um método GetAwaiter como aquele esperado pelo compilador. Portanto, como é que isso é compilado e executado com sucesso? Para melhor compreender isso, primeiramente, será necessário conhecer os tipo de .NET Task e Task<TResult> (a representação básica do Framework das operações assíncronas) e como eles se relacionam para esperar.

Convertendo para tasks (tarefas)

O .NET Framework 4.5 inclui todos os tipo de métodos necessário para suportar instâncias de esperas Task e Task<TResult> (Task<TResult> deriva de Task). Task e Task<TResult> expõem métodos de instância GetAwaiter que retorna respectivamente tipos TaskAwaiter e TaskAwaiter<TResult> que expõem os membros IsCompleted, OnCompleted e GetResult o suficiente para satisfazer o C# e os compiladores do Visual Basic. IsCompleted retorna um booliano indicando se a tarefa foi concluída no momento em que a propriedade é acessada. OnCompleted conecta na tarefa um delegado de continuação executado quando a tarefa é concluída (se a tarefa já tiver sido concluída quando OnCompleted é executado, a continuação delegada é agendada para execução assíncrona). GetResult retorna o resultado da tarefa se tiver terminado no estado TaskStatus.RanToCompletion (ele retorna nulo para o tipo Task não genérico), emite OperationCanceledException se a tarefa tiver terminado no estado TaskStatus.Canceled e emite quaisquer exceções que fazem com que a tarefa falhe se a tarefa tiver terminado no estado TaskStatus.Faulted.

Se tivermos um tipo personalizado cuja espera queremos suportar, teremos duas opções primárias. Uma opção é implementar todo o padrão de espera manualmente para o nosso tipo de espera personalizado, fornecendo um método GetAwaiter que retorna um tipo de awaiter personalizado que sabe como lidar com continuações e propagação de exceção. A segunda é implementar a capacidade de conversão do nosso tipo personalizado para uma tarefa e, então, apenas depender do suporte integrado para esperar que as tarefas esperem nosso tipo especial. Vamos explorar essa última abordagem.

O .NET Framework inclui um tipo chamado TaskCompletionSource<TResult>, que torna esses tipos de conversões mais objetivos. TaskCompletionSource<TResult> cria um objeto Task<TResult> e oferece a você os métodos SetResult, SetException, e SetCanceled que você usa para controlar diretamente quando e em qual estado a tarefa correspondente é concluída. Então, você pode usar um TaskCompletionSource<TResult> como um tipo de shim ou proxy para representar algumas operações assíncronas, como uma operação assíncrona do WinRT.

Vamos imaginar por um momento que você ainda não soubesse que seria possível esperar as operações do WinRT. Como você poderia habilitá-lo?

IAsyncOperation<string> op = SomeMethodAsync();
string result = await ...; // need something to await

Você poderia criar um TaskCompletionSource<TResult>, usá-lo como um proxy para representar a operação assíncrona do WinRT e esperar a tarefa correspondente. Vamos experimentar. Primeiro, precisamos instanciar um TaskCompletionSource<TResult> de maneira que possamos esperar seu Task:

IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
...
string result = await tcs.Task;

Então, assim como vimos em nosso exemplo anterior do uso manual de operações assíncronas dos manipuladores Completed do WinRT, precisaremos conectar um retorno de chamada à operação assíncrona para que saibamos quando ela será concluída:

IAsyncOperation<string> op = SomeMethodAsync();

var tcs = new TaskCompletionSource<TResult>();

op.Completed = delegate
{
...
};
string result = await tcs.Task;

E, então, nesse retorno de chamada, precisamos transferir o estado de conclusão do IAsyncOperation<TResult> para a tarefa:

IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
op.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
string result = await tcs.Task;

E é isso. As operações assíncronas do WinRT garantem que o manipulador Completed seja executado corretamente mesmo se o manipulador estiver inscrito após a conclusão da operação, de maneira que não precisará realizar nenhuma ação específica para inscrever o manipulador concorrendo com a conclusão da operação. As operações assíncronas do WinRT também procuram remover a referência ao manipulador Completed após a conclusão da operação, para que não seja preciso fazer nada especial para definir Completed como nulo quando o manipulador for executado. Na verdade, o manipulador Completed é definido uma vez. Isso significa que após defini-lo, você obtém um erro caso tente defini-lo novamente.

Com essa abordagem, haverá um mapeamento um-para-um entre como o estado no qual a operação assíncrona do WinRT é concluída e o estado no qual as tarefas de representação são concluídas:

AsyncStatus Terminal

Converte para TaskStatus

Que quando esperado...

Concluído

RanToCompletion

Retorna o resultado da operação (ou nulo)

Erro

Falha

Emite a exceção da operação que falhou

Cancelado

Cancelado

Emite uma OperationCanceledException

Claro, o código clichê escrito para manipular esta await ficaria rapidamente tedioso se tivéssemos de escrevê-lo toda vez que quiséssemos esperar uma operação assíncrona do WinRT. Como bons programadores, podemos encapsular o código clichê em um método que podemos usar inúmeras vezes. Façamos como um método de extensão que converte a operação assíncrona do WinRT em uma tarefa:

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation)
{
var tcs = new TaskCompletionSource<TResult>();
operation.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
return tcs.Task;
}

Com aquele método de extensão, posso escrever um código assim:

IAsyncOperation<string> op = SomeMethodAsync();
string result = await op.AsTask();

ou até mesmo mais simples:

string result = await SomeMethodAsync().AsTask();

Bem melhor. Claro, esse tipo de funcionalidade AsTask estará em alta demanda por qualquer um usando o WinRT do C# e do Visual Basic, para que você não precise escrever sua própria implementação: ainda há métodos embutidos no .NET 4.5. O assembly System.Runtime.WindowsRuntime.dll contém estes métodos de extensão para as interfaces assíncronas do WinRT:

namespace System
{
public static class WindowsRuntimeSystemExtensions
{
// IAsyncAction

public static Task AsTask(
this IAsyncAction source);
public static Task AsTask(
this IAsyncAction source,
CancellationToken cancellationToken);

// IAsyncActionWithProgress

public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
IProgress<TProgress> progress);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);

// IAsyncOperation

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source);
public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source,
CancellationToken cancellationToken);

// IAsyncOperationWithProgress

public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
IProgress<TProgress> progress);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);

...
}
}

Cada uma das quatro interfaces tem uma sobrecarga AsTask sem parâmetros semelhante ao que acabamos de escrever do zero. Além disso, cada um também tem uma sobrecarga que aceita um CancellationToken. Esse token é um mecanismo comum no .NET usado para fornecer cancelamento cooperativo e combinável. Você transmite um token para todas as suas operações assíncronas e quando o cancelamento é solicitado, todas aquelas operações assíncronas terão cancelamento solicitado. Apenas como exemplo (porque, como você já sabe, essa API já está disponível), como devemos criar nossa própria sobrecarga de AsTask (CancellationToken)? O CancellationToken fornece um método Register que aceita que um delegado seja executado quando um cancelamento é solicitado. Podemos, então, simplesmente fornecer um delegado que chamará Cancel no objeto IAsyncInfo, encaminhando a solicitação de cancelamento:

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation,
CancellationToken cancellationToken
{
using(cancellationToken.Register(() => operation.Cancel()))
return await operation.AsTask();
}

Embora a implementação fornecida no .NET 4.5 não seja exatamente assim, a lógica é a mesma.

Para IAsyncActionWithProgress<TProgress> e IAsyncOperationWithProgress<TResult,TProgress>, também há sobrecargas que aceitam um IProgress<TProgress>. IProgress<T> é uma interface .NET para a qual os métodos podem aceitar relatar progressos e o método AsTask simplesmente conecta um delegado para a propriedade Progress da operação assíncrona do WinRT para encaminhar a informação de progresso para IProgress. De novo, um exemplo de como isso deve ser implementado manualmente:

public static Task<TResult> AsTask<TResult,TProgress>(
this IAsyncOperationWithProgress<TResult> operation,
IProgress<TProgress> progress
{
operation.Progress += (_,p) => progress.Report(p);
return operation.AsTask();
}

Diretamente esperando uma operação assíncrona do WinRT

Vimos agora como é possível criar tarefas para representar uma operação assíncrona do WinRT que pode esperar aquelas tarefas. Mas que tal esperar diretamente as operações do WinRT? Em outras palavras, é possível escrever:

await SomeMethodAsync().AsTask();

mas para os casos onde não precisamos de suporte um CancellationToken ou um IProgress<T>, não seria bom evitar que a codificação chame AsTask?

await SomeMethodAsync();

Claro, isso é possível, como vimos no começo desta postagem. Lembre-se de como o compilador espera localizar um método um GetAwaiter que retorna um tipo de awaiter adequado? Como mencionado, o tipo WindowsRuntimeSystemExtensions no System.Runtime.WindowsRuntime.dll inclui métodos de extensão como GetAwaiter para quatro interfaces assíncronas do WinRT:

namespace System
{
public static class WindowsRuntimeSystemExtensions
{
...
public static TaskAwaiter GetAwaiter(
this IAsyncAction source);
public static TaskAwaiter<TResult> GetAwaiter<TResult>(
this IAsyncOperation<TResult> source);
public static TaskAwaiter GetAwaiter<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static TaskAwaiter<TResult> GetAwaiter<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
}
}

Observe o tipo de retorno de cada um desses métodos: TaskAwaiter ou TaskAwaiter<TResult>. Cada um desses métodos tira vantagem de um awaiter de tarefa existente embutido no Framework. Considerando o que você já sabe sobre AsTask, você pode provavelmente adivinhar como esses são implementados. A implementação real no Framework é quase assim:

public static TaskAwaiter GetAwaiter(
this IAsyncAction source)
{
return source.AsTask().GetAwaiter();
}

Isso significa que essas duas linhas resultam exatamente no mesmo comportamento:

await SomeMethodAsync().AsTask();
await SomeMethodAsync();

Personalizando um comportamento de espera

Como mencionado anteriormente, TaskAwaiter e TaskAwaiter<TResult> suportam todos os membros necessários para atender a expectativa do compilador de um awaiter:

bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult(); //returns void on TaskAwaiter

O membro mais interessante aqui é OnCompleted, pois é o responsável por executar o delegado de continuação quando a operação awaited é concluída. OnCompleted fornece um comportamento especial de marshaling para garantir que o delegado de continuação seja executado no lugar correto.

Por padrão, quando o OnCompleted do awaiter da tarefa é chamado, ele percebe o SynchronizationContext atual, que é uma representação abstrata do ambiente no qual o código é executado. Na thread da interface do usuário do aplicativo estilo Metro, SynchronizationContext.Current retorna uma instância do tipo interno WinRTSynchronizationContext . SynchronizationContext fornece um método Post virtual que aceita um delegado e executa esse delegado em um local apropriado para o contexto; WinRTSynchronizationContext encapsula um CoreDispatcher e usa seu RunAsync para executar um delegado assincronamente na thread da interface do usuário (como fizemos manualmente nesta postagem). Quando a tarefa awaited é concluída, o delegado transmitido para OnCompleted é Post'ed (lançado) para execução no SynchronizationContext capturado que era atual quando OnCompleted havia sido executado. Isso é o que o permite escrever códigos usando await em sua interface do usuário sem se preocupar em realizar marshaling para a thread correta: o awaiter da tarefa manipula por você.

Claro, podem haver situações específicas em que você não queira esse comportamento de marshaling padrão. Tais situações ocorrem com frequência nas bibliotecas: muitos tipos de bibliotecas não se importam em manipular controles de interface do usuário ou das threads específicas nas quais elas são executadas e, do ponto de vista do desempenho, é bastante útil poder evitar a sobrecarga associada a marshaling entre threads. Para acomodar códigos que querem desabilitar este comportamento de marshaling padrão, Task e Task<TResult> fornecem métodos ConfigureAwait. ConfigureAwait aceita um parâmetro booliano continueOnCapturedContext: passing true (transmissão verdadeira) significa usar o comportamento padrão e passing false (transmissão falsa) significa que o sistema não precisa forçar marshaling na execução do delegado para devolvê-la ao contexto original, podendo, em vez disso, executar um delegado no lugar mais adequado para o sistema.

Considerando isso, se você quiser esperar uma operação de WinRT sem forçar o resto da execução de volta para a thread da interface do usuário em vez de escrever:

await SomeMethodAsync();

ou:

await SomeMethodAsync().AsTask();

você pode escrever:

await SomeMethodAsync().AsTask()
.ConfigureAwait(continueOnCapturedContext:false);

ou apenas:

await SomeMethodAsync().AsTask().ConfigureAwait(false);

Quando usar AsTask

Se tudo o que você quiser fazer é executar uma operação assíncrona de WinRT e esperar sua conclusão, a abordagem mais simples é esperar diretamente a operação assíncrona do WinRT:

await SomeMethodAsync();

Mas assim que quiser mais controle, você precisará usar AsTask. Você já viu alguns casos onde isto é útil:

  • Suportando o cancelamento via CancellationToken

    CancellationToken token = ...;
    await SomeMethodAsync().AsTask(token);

  • Suportando o relatório do progresso via IProgress<T>
    IProgress<TProgress> progress = ...;
    await SomeMethodAsync().AsTask(progress);
  • Suprimindo a continuação do comportamento de marshaling padrão via ConfigureAwait
    await SomeMethodAsync().AsTask().ConfigureAwait(false);

Também há situações adicionais importantes onde AsTask é bastante útil.

Uma situação tem a ver com a capacidade do Taskde suportar múltiplas continuações. Os tipos de operação assíncrona do WinRT suportam apenas um único delegado registrado com Completed (Completed é uma propriedade, e não um evento) e só podem ser definidos uma vez. Isso é aceitável para a maioria dos casos nos quais você simplesmente quer esperar a operação uma vez, por exemplo, em vez de chamar um método assíncrono:

SomeMethod();

você chama e espera um equivalente assíncrono:

await SomeMethodAsync();

mantendo, é claro, o mesmo fluxo de controle como se você usasse um equivalente assíncrono. Mas às vezes você quer ser capaz de conectar múltiplas chamadas de retorno, ou você quer ser capaz de esperar a mesma instância muitas vezes. Em contraste às interfaces assíncronas do WinRT, o tipo Task não oferece suporte para ficar em espera por um número indefinido de vezes e/ou ter o seu método ContinueWith usado a qualquer momento para suportar um número ilimitado de chamadas de retorno. Sendo assim, é possível usar AsTask para obter uma tarefa para a sua operação assíncrona do WinRT e depois conectar suas múltiplas chamadas de retorno ao Task em vez de conectar à operação assíncrona do WinRT diretamente.

var t = SomeMethodAsync().AsTask();
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });

Outro exemplo de onde AsTask pode ser útil é ao lidar com métodos que operam com os tipos Task ou Task<TResult>. Métodos de combinadores como Task.WhenAll ou Task.WhenAny operam com Task, não com interfaces assíncronas do WinRT. Portanto, se você quer poder executar múltiplas operações assíncronas do WinRT e depois await (esperar) até que todas sejam concluídas, você poderia usar AsTask para facilitar. Por exemplo, esse await é concluído assim que qualquer uma das três operações suportadas seja concluída e retorna o Task representando a operação concluída:

Task firstCompleted = await Task.WhenAny(
SomeMethod1Async().AsTask(),
SomeMethod2Async().AsTask(),
SomeMethod3Async().AsTask());

Conclusão

É interessante notar as muitas funcionalidades que o WinRT fornece via operações assíncronas; o volume dessas APIs expostas demonstram o quanto a capacidade de resposta é importante para a plataforma. Isso coloca uma demanda significativa no modelo de programação usado para funcionar com essas operações: para C# e códigos Visual Basic, await e AsTask cumprem muito bem os seus papéis. Esta postagem do blog procurou simplificar o assunto ao máximo e oferecer a você uma boa noção de como esses recursos funcionam para permitir o desenvolvimento de aplicativos estilo Metro de maneira mais produtiva.

Para obter mais informações, recomendo os seguintes recursos:

Stephen Toub
Visual Studio