Diferença entre await e ContinueWith

119

Alguém pode explicar se awaite ContinueWithsão sinônimos ou não no exemplo a seguir. Estou tentando usar o TPL pela primeira vez e tenho lido toda a documentação, mas não entendo a diferença.

Aguarde :

String webText = await getWebPage(uri);
await parseData(webText);

ContinueWith :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

É um preferido sobre o outro em situações particulares?

Harrison
fonte
3
Se você removeu a Waitchamada no segundo exemplo , os dois fragmentos seriam (em sua maioria) equivalentes.
Servy
FYI: Seu getWebPagemétodo não pode ser usado em ambos os códigos. No primeiro código, ele possui um Task<string>tipo de retorno, enquanto no segundo ele possui um stringtipo de retorno. então, basicamente, seu código não compila. - se para ser preciso.
Royi Namir

Respostas:

101

No segundo código, você está aguardando de forma síncrona a conclusão da continuação. Na primeira versão, o método retornará ao chamador assim que atingir a primeira awaitexpressão que ainda não foi concluída.

Eles são muito semelhantes no sentido de que ambos agendam uma continuação, mas assim que o fluxo de controle se torna ainda mais complexo, awaitleva a um código muito mais simples. Além disso, conforme observado por Servy nos comentários, aguardar uma tarefa "desembrulhará" exceções agregadas, o que geralmente leva a um tratamento de erros mais simples. O uso também awaitagendará implicitamente a continuação no contexto de chamada (a menos que você use ConfigureAwait). Não é nada que não possa ser feito "manualmente", mas é muito mais fácil fazê-lo com await.

Eu sugiro que você tente implementar uma sequência ligeiramente maior de operações com ambos awaite Task.ContinueWith- pode ser uma verdadeira revelação.

Jon Skeet
fonte
2
O tratamento de erros entre os dois snippets também é diferente; geralmente é mais fácil trabalhar com o awaitover ContinueWithnesse aspecto.
Servy
@Servy: Verdade, acrescentarei algo em torno disso.
Jon Skeet
1
A programação também é bem diferente, ou seja, em que contexto é parseDataexecutado.
Stephen Cleary
Quando você diz que o uso de await irá agendar implicitamente a continuação no contexto de chamada , você pode explicar o benefício disso e o que acontece na outra situação?
Harrison
4
@Harrison: Imagine que você está escrevendo um aplicativo WinForms - se você escrever um método assíncrono, por padrão, todo o código dentro do método será executado no thread de interface do usuário, porque a continuação será agendada lá. Se você não especificar onde deseja que a continuação seja executada, não sei qual é o padrão, mas pode facilmente acabar rodando em um thread de pool de threads ... ponto em que você não pode acessar a IU, etc. .
Jon Skeet
100

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 é?

pkt
fonte
Obrigado, explicação muito boa
Elger Mensonides
Este é um ótimo exemplo
Royi Namir