Чем функция асинхронного ожидания в C # 5.0 отличается от TPL? - PullRequest
61 голосов
/ 29 октября 2010

Я не вижу различий между новыми асинхронными функциями C # (и VB) и .NET 4.0 Task Parallel Library . Взять, к примеру, код Эрика Липперта отсюда :

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 ]

71 голосов
/ 29 октября 2010

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

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

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

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

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

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

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

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

25 голосов
/ 29 октября 2010

Существует огромная разница:

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

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

24 голосов
/ 29 октября 2010

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

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

TPL, а точнее, Задача - это в одну сторону , которую вы можете использовать для фактического одновременного выполнения операций.Новые ключевые слова async и await позволяют составлять эти параллельные операции «синхронно» или «линейно».

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

6 голосов
/ 29 октября 2010

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

Посмотрите это видео на канале 9 Андерса, рассказывающего о новой функции.

4 голосов
/ 29 октября 2010

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

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

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

0 голосов
/ 07 января 2015

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

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

0 голосов
/ 29 октября 2010

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

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

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

Ключвозвращение контроля вызывающей стороне во время выполнения работы тяжеловесами.

...