Чем функция async-await в 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 для работы только с одним потоком.

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


person zildjohn01    schedule 29.10.2010    source источник
comment
Также см. stackoverflow.com/ questions / 3513432 / и stackoverflow.com / questions / 12414601 /   -  person nawfal    schedule 14.05.2013
comment
См. Ссылку с интересными примерами: richnewman.wordpress.com/2012/12/03/   -  person nawfal    schedule 15.05.2013


Ответы (7)


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

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

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

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

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

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

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

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

person Reed Copsey    schedule 29.10.2010

Это огромная разница:

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

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

person Daniel    schedule 29.10.2010

Андерс свел это к очень сжатому ответу в интервью Channel 9 Live, которое он дал. Я очень рекомендую это

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

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

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

person Brad Cunningham    schedule 29.10.2010
comment
На самом деле это ничего не говорит, не так ли? Позвольте мне перефразировать: как это отвечает на поставленный вопрос? - person Lasse V. Karlsen; 30.10.2010
comment
Вопрос в том, чем функция async-await в C # 5.0 отличается от TPL? Мой ответ подходит на этот вопрос ИМО - person Brad Cunningham; 30.10.2010

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

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

person John Leidegren    schedule 29.10.2010

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

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

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

person JaredPar    schedule 29.10.2010
comment
Этот код имел бы гораздо больший смысл, если бы архив был членом или статической переменной и не был определен в методе ... При этом асинхронные функции, возвращающие void, совершенно допустимы и приемлемы и имеют значение срабатывания и забвения, согласно документация по спецификации. - person Reed Copsey; 29.10.2010
comment
Вы хотите сказать, что вызов ArchiveDocuments() будет выглядеть как Task task = ArchiveDocuments(List<Url> urls);? Это кажется неправильным ... и, если я правильно понимаю, звонящий вообще не ждет. Фактически, если его волнует результат ArchiveDocuments(), весь сценарий развалится и не сработает. - person James King; 29.10.2010
comment
@Reed, я согласен, что они действительно верны и верны. Я просто считаю это очень вводящим в заблуждение и немного самонадеянным, потому что, может быть, я не хочу забывать;) - person JaredPar; 29.10.2010
comment
@James, я говорю, что я думаю, разница будет немного больше, если вы считаете, что Task также является допустимым возвратом. - person JaredPar; 29.10.2010

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

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

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

Ключ - вернуть управление вызывающему, пока тяжелоатлеты выполняют упражнения.

person James King    schedule 29.10.2010
comment
Обратите внимание, что тяжелые атлеты могут не работать. Они могут вообще не быть параллельны. Это могут быть единицы работы, которые должны быть запланированы в этом потоке, например, всякий раз, когда этот поток простаивает. Я вижу много смешения асинхронности с параллелизмом, и мне не терпится разубедить людей в этом понятии; асинхронность часто заключается в том, чтобы не производить больше рабочих потоков. Ключевым моментом является возвращение управления вызывающему после тяжелоатлетов, сделавших что-то, чтобы гарантировать, что их задача завершится в какой-то момент в будущем. - person Eric Lippert; 30.10.2010
comment
Поймите, о чем вы говорите, и согласитесь, что метод await ed вообще не требует запуска или планирования чего-либо. Что побудило меня сделать это заявление, так это archive.Wait(). В этом примере код определенно написан для ожидания, пока поток что-то сделает немедленно и завершит. Мое заявление имелось в виду только в контексте этого примера. - person James King; 01.11.2010
comment
@JamesKing, относительно ... вообще не нужно запускать или планировать что-либо: он в конечном итоге будет планировать или запускать s / t, верно? - person bvgheluwe; 18.10.2017

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

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

person Tb.    schedule 07.01.2015