Подтвердить что ты не робот

Разница между ожиданием и продолжением

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

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

4b9b3361

Ответ 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;
        }
    }
}

Это намного лучше, не так ли?