Как функция асинхронного ожидания C# 5.0 отличается от TPL?


я не вижу разницы между новыми асинхронными функциями C#(и 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 для работы только с одним нитка.

я явно упускаю что-то огромное; кто-нибудь может помочь мне понять это немного лучше?

7 59

7 ответов:

Я думаю, что непонимание возникает здесь:

Кажется, что ключевое слово await служит двум различным целям. Первое вхождение (FetchAsync), по-видимому, означает: "если это значение используется позже в методе и его задача не завершена, подождите, пока она не завершится, прежде чем продолжить."Второй экземпляр (архив), по-видимому, означает: "если эта задача еще не завершена, подождите прямо сейчас, пока она не завершится."Если я ошибаюсь, пожалуйста, поправьте меня.

Это на самом деле совершенно неверно. Оба они имеют одно и то же значение.

в первом случае:

var document = await FetchAsync(urls[i]);

что здесь происходит, так это то, что среда выполнения говорит: "начните вызывать FetchAsync, а затем верните текущую точку выполнения потоку, вызывающему этот метод."Здесь нет" ожидания " - вместо этого выполнение возвращается в контекст синхронизации вызова, и все продолжает сбиваться. В какой-то момент в будущем задача FetchAsync будет завершена, и в этот момент этот код возобновится в контексте синхронизации вызывающего потока, и произойдет следующая инструкция (назначение переменной документа).

выполнение будет продолжаться до второго вызова await - в это время произойдет то же самое - если Task<T> (архив) не завершен, выполнение будет выпущено в вызывающий контекст - в противном случае архив будет установлен.

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

есть огромная разница:

Wait() блоки await не блокирует. Если вы запустите асинхронную версию ArchiveDocuments() в вашем потоке GUI графический интерфейс будет оставаться отзывчивым во время выполнения операций выборки и архивирования. Если вы используете версию TPL с Wait(), ваш интерфейс будет заблокирован.

отметим, что async удается сделать это без введения каких-либо потоков - в точке await, управление просто возвращается в цикл обработки сообщений. После того, как задача ожидание завершено, остальная часть метода (продолжение) помещается в очередь в цикле сообщений, и поток GUI продолжит работу ArchiveDocuments где он остановился.

Андерс свел его к очень лаконичному ответу в прямом эфире 9-го канала интервью, которое он сделал. Я очень рекомендую его

новые ключевые слова Async и await позволяют вам организовать параллелизм в ваши приложения. Они фактически не вводят параллелизм в ваше приложение.

TPL и более конкретно задача один из способов вы можете использовать для выполнения операций одновременно. Новое ключевое слово async и await позвольте вам compose эти параллельные операции в "синхронном" или "линейном" режиме.

таким образом, вы все еще можете написать линейный поток управления в своих программах, в то время как фактические вычисления могут происходить или не происходить одновременно. Когда вычисление происходит одновременно, await и async позволяют вам compose эти операции.

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

проверить этот канал 9 видео Андерс говорит о новой функции.

проблема здесь в том, что подпись ArchiveDocuments вводит в заблуждение. Он имеет явное возвращение void но на самом деле возвращение Task. Для меня пустота подразумевает синхронность, поскольку нет способа "дождаться" ее завершения. Рассмотрим альтернативную сигнатуру функции.

async Task ArchiveDocuments(List<Url> urls) { 
  ...
}

для меня, когда оно написано таким образом, разница гораздо более очевидна. Элемент ArchiveDocuments функция не является той, которая завершается синхронно, но завершится позже.

вызов FetchAsync() будет по-прежнему блокировать до его завершения (если оператор в вызовах await?) Ключ заключается в том, что управление возвращается вызывающему абоненту (потому что ArchiveDocuments сам метод объявляется как async). Таким образом, вызывающий может с радостью продолжать обработку логики пользовательского интерфейса, реагировать на события и т. д.

, когда FetchAsync() завершает, он прерывает вызывающего абонента, чтобы закончить цикл. Он бьет ArchiveAsync() и блоки, но ArchiveAsync() вероятно, просто создает новую задачу, запускает ее и возвращает задача. Это позволяет начать второй цикл, пока задача обрабатывается.

второй цикл ударов FetchAsync() и блоки, возвращающие управление вызывающему абоненту. Когда FetchAsync() завершается, он снова прерывает абонента для продолжения обработки. Затем он попадает await archive, который возвращает управление вызывающему до Task создано в цикле 1 завершается. Как только эта задача завершена, вызывающий абонент снова прерывается, и второй цикл вызывает ArchiveAsync(), который получает начатую задачу и начинается цикл 3, повторить реклама.

ключ возвращает управление вызывающему абоненту во время выполнения тяжелых подъемников.

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

чтобы увидеть, как будет выглядеть код ожидания без "await", см. эту отличную ссылку: http://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await.aspx