Aqui está a sequência de trechos de código que usei recentemente para ilustrar a diferença e vários problemas usando soluções assíncronas.
Suponha que você tenha algum manipulador de eventos em seu aplicativo baseado em GUI que leve muito tempo e, portanto, você gostaria de torná-lo assíncrono. Esta é a lógica síncrona com a qual você começa:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem retorna uma Task, que eventualmente produzirá algum resultado que você gostaria de inspecionar. Se o resultado atual for o que você está procurando, atualize o valor de algum contador na IU e retorne do método. Caso contrário, você continuará processando mais itens de LoadNextItem.
Primeira ideia para a versão assíncrona: use apenas continuações! E vamos ignorar a parte do loop por enquanto. Quer dizer, o que poderia dar errado?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
Ótimo, agora temos um método que não bloqueia! Em vez disso, ele trava. Quaisquer atualizações nos controles da IU devem acontecer no encadeamento da IU, portanto, você precisará levar em conta isso. Felizmente, há uma opção para especificar como as continuações devem ser agendadas, e há um padrão apenas para isso:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Ótimo, agora temos um método que não trava! Em vez disso, ele falha silenciosamente. As continuações são tarefas separadas, com seu status não vinculado ao da tarefa anterior. Portanto, mesmo se LoadNextItem falhar, o chamador verá apenas uma tarefa que foi concluída com êxito. Ok, então apenas passe a exceção, se houver uma:
return LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
throw t.Exception.InnerException;
}
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Ótimo, agora isso realmente funciona. Para um único item. Agora, que tal aquele loop. Acontece que uma solução equivalente à lógica da versão síncrona original será mais ou menos assim:
Task AsyncLoop() {
return AsyncLoopTask().ContinueWith(t =>
Counter.Value = t.Result,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
var tcs = new TaskCompletionSource<int>();
DoIteration(tcs);
return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
tcs.TrySetException(t.Exception.InnerException);
} else if (t.Result.Contains("target")) {
tcs.TrySetResult(t.Result.Length);
} else {
DoIteration(tcs);
}});
}
Ou, em vez de todos os itens acima, você pode usar o assíncrono para fazer a mesma coisa:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
Isso é muito melhor agora, não é?
Wait
chamada no segundo exemplo , os dois fragmentos seriam (em sua maioria) equivalentes.