Разница между Task и async

С# предоставляет два способа создания асинхронных методов:

Метод 1:

static Task<string> MyAsyncTPL() {
  Task<string> result = PerformWork();
  return result.ContinueWith(t => MyContinuation());
}

Метод 2:

static async Task<string> MyAsync() {
  string result = await PerformWork();
  return MyContinuation();
}

Оба вышеуказанных метода являются асинхронными и реализуют одно и то же. Итак, когда я должен выбрать один метод над другим? Существуют ли какие-либо рекомендации или преимущества использования одного над другим?

Ответы

Ответ 1

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

Когда вы используете ContinueWith, вы выбираете абстракцию более низкого уровня. В частности, вот некоторые "опасные точки", поэтому я не рекомендую использовать ContinueWith, если метод действительно не прост (или ваше имя: Stephen Toub):

  • Исключения из методов async Task помещаются в возвращаемую задачу; исключения, полученные из методов не async, распространяются напрямую.
  • await по умолчанию возобновит метод async в том же "контексте". Этот "контекст" SynchronizationContext.Current, если он не равен null, и в этом случае это TaskScheduler.Current. Это означает, что если вы вызываете MyAsync в потоке пользовательского интерфейса (или в контексте запроса ASP.NET), тогда MyContinuation также будет выполняться в потоке пользовательского интерфейса (или в том же контексте запроса ASP.NET). Я объясняю это более в своем блоге.
  • Вы всегда должны указывать планировщик для ContinueWith; в противном случае он подберет TaskScheduler.Current, что может вызвать удивительное поведение. Я подробно описываю эту проблему в своем блоге. Это сообщение о StartNew; но ContinueWith имеет ту же самую "планировщик по умолчанию по умолчанию", описанную в этой статье.
  • await использует соответствующие флаги поведения и оптимизации, которые по умолчанию не установлены в ContinueWith. Например, он использует DenyChildAttach (чтобы асинхронные задачи не были ошибочно использованы в качестве параллельных задач) и ExecuteSynchronously (оптимизация).

Короче говоря, единственной причиной использования ContinueWith для асинхронных задач является сохранение чрезвычайно небольшого количества времени и памяти (из-за отсутствия служебных обязанностей конечного автомата async), а взамен ваш код менее читабельный и поддерживаемый.

С очень простым примером вы можете уйти от него; но, как указал Джон Скит, как только у вас появятся циклы, код ContinueWith просто взрывается по сложности.

Ответ 2

await в основном является сокращением для продолжения, по умолчанию с использованием одного и того же контекста синхронизации для продолжения.

Для очень простых примеров, подобных вашим, не так много пользы при использовании await - хотя обертка и развертывание исключений делает более последовательный подход.

Если у вас есть более сложный код, async имеет огромное значение. Представьте, что вы хотели:

static async Task<List<string>> MyAsync() {
    List<string> results = new List<string>();
    // One at a time, but each asynchronously...
    for (int i = 0; i < 10; i++) {
        // Or use LINQ, with rather a lot of care :)
        results.Add(await SomeMethodReturningString(i));
    }
    return results;
}

..., который становится более увлекательным с ручным продолжением.

Кроме того, async/await может работать с типами, отличными от Task/Task<T>, пока они реализуют соответствующий шаблон.

Стоит прочитать больше о том, что он делает за кулисами. Возможно, вы захотите начать с MSDN.