Разница между await и ContinueWith

119

Может ли кто-нибудь объяснить, являются ли awaitи ContinueWithсинонимами или нет, в следующем примере. Я пытаюсь использовать TPL впервые, читал всю документацию, но не понимаю разницы.

Жду :

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();

Что предпочтительнее в определенных ситуациях?

Харрисон
источник
3
Если вы удалили Waitвызов во втором примере , то два сниппет будут ( в основном) эквивалентны.
Servy
К вашему сведению: ваш getWebPageметод нельзя использовать в обоих кодах. В первом коде он имеет Task<string>тип возврата, а во втором - stringтип возврата. так что в основном ваш код не компилируется. - если быть точным.
Рой Намир

Ответы:

101

Во втором коде вы синхронно ждете завершения продолжения. В первой версии метод вернется к вызывающему, как только он встретит первое awaitвыражение, которое еще не завершено.

Они очень похожи в том, что оба планируют продолжение, но как только поток управления становится даже немного сложным, это awaitприводит к гораздо более простому коду. Кроме того, как отметил Серви в комментариях, ожидание задачи будет «разворачивать» совокупные исключения, что обычно приводит к более простой обработке ошибок. Также использование awaitбудет неявно запланировать продолжение в вызывающем контексте (если вы не используете ConfigureAwait). Нет ничего, что нельзя было бы сделать «вручную», но с этим сделать это намного проще await.

Я предлагаю вам попробовать реализовать чуть большую последовательность операций с обоими awaitи Task.ContinueWith- это может быть настоящим откровением.

Джон Скит
источник
2
Обработка ошибок между двумя фрагментами также различается; в этом отношении, как правило, легче работать с awaitболее ContinueWith.
Servy
@Servy: Верно, кое-что добавлю по этому поводу.
Джон Скит,
1
Планирование также совершенно другое, то есть в каком контексте parseDataвыполняется.
Стивен Клири
Когда вы говорите, что использование await будет неявно запланировать продолжение в контексте вызова , можете ли вы объяснить преимущества этого и что происходит в другой ситуации?
Харрисон
4
@Harrison: Представьте, что вы пишете приложение WinForms - если вы пишете асинхронный метод, по умолчанию весь код внутри метода будет выполняться в потоке пользовательского интерфейса, потому что продолжение будет запланировано там. Если вы не укажете, где вы хотите запустить продолжение, я не знаю, что по умолчанию, но оно может легко закончиться запуском в потоке пула потоков ... в этот момент вы не можете получить доступ к пользовательскому интерфейсу и т. Д. .
Джон Скит
100

Вот последовательность фрагментов кода, которые я недавно использовал, чтобы проиллюстрировать разницу и различные проблемы с использованием асинхронного решения.

Предположим, у вас есть обработчик событий в вашем приложении на основе графического интерфейса пользователя, который требует много времени, и поэтому вы хотите сделать его асинхронным. Вот синхронная логика, с которой вы начинаете:

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem возвращает задачу, которая в конечном итоге даст результат, который вы хотите проверить. Если текущий результат - это тот, который вы ищете, вы обновляете значение некоторого счетчика в пользовательском интерфейсе и возвращаетесь из метода. В противном случае вы продолжите обработку дополнительных элементов из LoadNextItem.

Первая идея для асинхронной версии: просто используйте продолжения! И давайте пока проигнорируем часть зацикливания. Я имею в виду, что могло пойти не так?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

Отлично, теперь у нас есть метод, который не блокирует! Вместо этого происходит сбой. Любые обновления элементов управления пользовательского интерфейса должны происходить в потоке пользовательского интерфейса, поэтому вам нужно будет это учитывать. К счастью, есть возможность указать, как следует планировать продолжения, и есть вариант по умолчанию для этого:

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Отлично, теперь у нас есть метод, который не дает сбоев! Вместо этого он тихо терпит неудачу. Продолжения сами по себе являются отдельными задачами, их статус не привязан к статусу предшествующей задачи. Таким образом, даже если LoadNextItem выдает ошибку, вызывающий увидит только успешно завершенную задачу. Хорошо, тогда просто передайте исключение, если оно есть:

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());

Отлично, теперь это действительно работает. Для одного предмета. Теперь, как насчет этого цикла. Оказывается, решение, эквивалентное логике исходной синхронной версии, будет выглядеть примерно так:

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);
        }});
}

Или вместо всего вышеперечисленного вы можете использовать async, чтобы сделать то же самое:

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

Теперь это намного лучше, не так ли?

ПКТ
источник
Спасибо, действительно хорошее объяснение
Элгер Менсонидес
Это отличный пример
Рой Намир