Как функция асинхронного ожидания С# 5.0 отличается от TPL?
Я не вижу различий между новыми функциями async С# (и VB) и .NET 4.0 Параллельная библиотека задач. Возьмем, к примеру, код Эрика Липперта отсюда:
async void ArchiveDocuments(List<Url> urls) {
Task archive = null;
for(int i = 0; i < urls.Count; ++i) {
var document = await FetchAsync(urls[i]);
if (archive != null)
await archive;
archive = ArchiveAsync(document);
}
}
Кажется, что ключевое слово await
выполняет две разные цели. Первое вхождение (FetchAsync
), по-видимому, означает: "Если это значение используется позже в методе, и его задача еще не закончена, подождите, пока она не завершится до продолжения". Второй экземпляр (archive
), по-видимому, означает: "Если эта задача еще не закончена, подождите прямо сейчас, пока она не завершится". Если я ошибаюсь, пожалуйста, исправьте меня.
Не так ли легко написать так?
void ArchiveDocuments(List<Url> urls) {
for(int i = 0; i < urls.Count; ++i) {
var document = FetchAsync(urls[i]); // removed await
if (archive != null)
archive.Wait(); // changed to .Wait()
archive = ArchiveAsync(document.Result); // added .Result
}
}
Я заменил первый await
на Task.Result
, где действительно нужно значение, а второе await
с Task.Wait()
, где на самом деле происходит ожидание. Функциональность (1)
уже реализована, а (2)
гораздо ближе семантически к тому, что на самом деле происходит в коде.
Я понимаю, что метод async
перезаписывается как конечный автомат, похожий на итераторы, но я также не вижу, какие выгоды приносят. Любой код, которому требуется другой поток для работы (например, загрузка), по-прежнему будет нуждаться в другом потоке, а любой код, который не работает (например, чтение из файла), все еще может использовать TPL для работы только с одним потоком.
Мне явно не хватает чего-то огромного здесь; может ли кто-нибудь помочь мне понять это немного лучше?
Ответы
Ответ 1
Я думаю, что здесь возникает недоразумение:
Кажется, что ключевое слово await выполняет две разные цели. Первое вхождение (FetchAsync), по-видимому, означает: "Если это значение используется позже в методе и его задача не закончена, подождите, пока она не завершится, прежде чем продолжить". Второй экземпляр (архив), похоже, означает: "Если эта задача еще не закончена, подождите прямо сейчас, пока она не завершится". Если я ошибаюсь, пожалуйста, исправьте меня.
Это абсолютно неверно. Оба они имеют одинаковый смысл.
В вашем первом случае:
var document = await FetchAsync(urls[i]);
Что здесь происходит, так это то, что среда выполнения говорит "Начать вызов FetchAsync, а затем вернуть текущую точку выполнения в поток, вызывающий этот метод". Здесь нет "ожидания" - вместо этого выполнение возвращается к контексту синхронизации вызова, и все продолжает падать. В какой-то момент в будущем задача FetchAsync завершится, и в этот момент этот код возобновится в контексте синхронизации вызывающего потока, и произойдет следующее выражение (назначение переменной документа).
Выполнение будет продолжаться до второго ожидания - в это время произойдет то же самое - если Task<T>
(архив) не будет завершен, выполнение будет выпущено в вызывающий контекст - в противном случае архив будет быть установленным.
Во втором случае все очень по-другому: здесь вы явно блокируете, а это означает, что контекст синхронизации вызовов никогда не сможет выполнить какой-либо код до завершения всего вашего метода. Конечно, есть еще асинхронность, но асинхронизация полностью содержится в этом блоке кода - никакой код за пределами этого вставленного кода не будет происходить в этом потоке до тех пор, пока весь ваш код не завершится.
Ответ 2
Существует огромная разница:
Wait()
блоки, await
не блокируется. Если вы запускаете асинхронную версию ArchiveDocuments()
в потоке графического интерфейса пользователя, графический интерфейс будет оставаться отзывчивым при выполнении операций выборки и архивирования.
Если вы используете версию TPL с Wait()
, ваш графический интерфейс будет заблокирован.
Обратите внимание, что async
удается сделать это без введения каких-либо потоков - в точке await
элемент управления просто возвращается в цикл сообщения. После того, как задача, ожидающая завершения, завершена, оставшаяся часть метода (продолжение) помещается в очередь в контуре сообщения, и поток GUI будет продолжать работать ArchiveDocuments
, где он остановился.
Ответ 3
Андерс довел до очень сжатого ответа в интервью Channel 9 Live, которое он сделал. Я очень рекомендую его
Новые ключевые слова Async и ожидания позволяют вам организовать concurrency в ваших приложениях. Они фактически не вводят в приложение concurrency.
TPL и более конкретно Задача в одну сторону, которую вы можете использовать для фактического выполнения операций одновременно. Новое ключевое слово async и await позволяет составлять эти параллельные операции в режиме "синхронный" или "линейный".
Таким образом, вы можете написать линейный поток управления в своих программах, в то время как фактические вычисления могут или не могут произойти одновременно. Когда вычисления выполняются одновременно, ждут и асинхронно, вы можете составить эти операции.
Ответ 4
Возможность превратить поток управления программой в конечный автомат - вот что делает эти новые ключевые слова интересными. Подумайте об этом как , получившем управление, а не значения.
Посмотрите этот видеоролик 9-го канала Андерса, рассказывающего о новой функции.
Ответ 5
Проблема заключается в том, что подпись ArchiveDocuments
вводит в заблуждение. Он имеет явный возврат void
, но на самом деле возврат Task
. Для меня void подразумевает синхронность, поскольку нет возможности "подождать", чтобы она закончилась. Рассмотрим альтернативную сигнатуру функции.
async Task ArchiveDocuments(List<Url> urls) {
...
}
Для меня, когда это написано таким образом, разница гораздо более очевидна. Функция ArchiveDocuments
не является синхронной, но завершается позже.
Ответ 6
Вызов FetchAsync()
по-прежнему будет блокироваться до тех пор, пока он не завершится (если оператор не вызывает вызовы await
?) Ключ в том, что элемент управления возвращается вызывающему (поскольку сам метод ArchiveDocuments
объявлен как async
). Таким образом, вызывающий может с радостью продолжать обработку логики пользовательского интерфейса, реагировать на события и т.д.
Когда FetchAsync()
завершается, он прерывает вызывающего абонента, чтобы закончить цикл. Он попадает в ArchiveAsync()
и блокирует, но ArchiveAsync()
, вероятно, просто создает новую задачу, запускает ее и возвращает задачу. Это позволяет начать второй цикл, в то время как задача обрабатывается.
Второй цикл обращается к FetchAsync()
и блокирует, возвращая управление вызывающему. Когда FetchAsync()
завершается, он снова прерывает вызывающего абонента, чтобы продолжить обработку. Затем он обращается к await archive
, который возвращает управление вызывающему абоненту до завершения Task
, созданного в цикле 1. По завершении этой задачи вызывающий абонент снова прерывается, а второй цикл вызывает ArchiveAsync()
, который запускает задачу и начинает цикл 3, повторяет объявление.
Ключ возвращает управление вызывающему абоненту во время выполнения тяжелых лифтеров.
Ответ 7
Ключевое слово ожидания не вводит concurrency. Это похоже на ключевое слово yield, оно сообщает компилятору, чтобы он перестроил ваш код в лямбда, управляемый конечным автоматом.
Чтобы увидеть, какой код ожидания будет выглядеть без "ожидания", см. эту прекрасную ссылку: http://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await.aspx