Разница между ожиданием и продолжением
Может кто-нибудь объяснить, являются ли синтаксические символы await
и ContinueWith
в следующем примере. Я пытаюсь использовать TPL в первый раз и читал всю документацию, но не понимаю разницы.
Await
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();
В некоторых ситуациях предпочтительнее другого?
Ответы
Ответ 1
Во втором коде вы синхронно ожидаете завершения продолжения. В первой версии метод вернется к вызывающему, как только он достигнет первого выражения await
, которое еще не завершено.
Они очень похожи на то, что оба планируют продолжение, но как только поток управления становится даже немного сложным, await
приводит к гораздо более простому коду. Кроме того, как заметил Servy в комментариях, ожидание задачи "разворачивает" совокупные исключения, что обычно приводит к более простой обработке ошибок. Также использование await
будет неявно планировать продолжение в вызывающем контексте (если вы не используете ConfigureAwait
). Это ничего, что нельзя сделать "вручную", но гораздо проще сделать это с помощью await
.
Я предлагаю вам попробовать выполнить несколько большую последовательность операций с await
и Task.ContinueWith
- это может стать настоящим открытием.
Ответ 2
Здесь последовательность фрагментов кода, которые я недавно использовал, чтобы проиллюстрировать разницу и различные проблемы с помощью async.
Предположим, что у вас есть обработчик событий в приложении на основе графического интерфейса, для которого требуется много времени, и поэтому вы хотите сделать его асинхронным. Здесь начинается синхронная логика:
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;
}
}
}
Это намного лучше, не так ли?