it-swarm.com.ru

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

Может кто-нибудь объяснить, являются ли 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();

Является ли один предпочтительным по сравнению с другим в конкретных ситуациях?

101
Harrison

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

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

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

85
Jon Skeet

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

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

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

Это намного приятнее, не так ли?

83
pkt