Изучаю асинхронное программирование и вижу следующий метод async Task Produce(ITargetBlock queue, int howmuch) { Random r = new Random() while (howmuch-- > 0) { await Task.Delay(1000 * r.Next(1, 3)) var v = string.Format("automatic {0}", r.Next(1, 10)) await queue.SendAsync(v) } queue.Complete() } Что-то не так с этим методом, но что не могу понять. Кажется, async и await лишние. Непонимаю для чего надо вызывать await Task.Delay? Про то, что это для иммитации бурной деятелности это понятно. Вопрос о другом: если надо остановить текущий поток, то для чего запускать другой поток? Почему не сделано просто: Task.Delay(1000 * r.Next(1, 3)).Wait() ? Если вызвали Delay, то значит надо подождать указанное время, то есть надо остановить текущий поток. А из-за await получается какая-то ерунда, так как поток вызывает другой поток, чтобы ждать в нем, и сам ждет другой поток. Если метод возвращает Task, то почему нет return?
Ответ Зачем это нужно Давайте начнем с того, зачем вообще появилась нужда в async/await. Представим, в приложении есть сетевой вызов, занимающий время. Или нужно записать большой файл на диск. Секрет в том, что в тот момент, когда вызов уходит на устройство (будь то сетевая карта или жесткий диск), текущий поток блокируется до тех пор, пока не придет ответ (т.е. пока не придет ответ от сервера или все данные не сбросятся на диск). Это расточительное использование ресурсов, поскольку в это время текущий поток ничего не делает, но мог бы заниматься другой работой. Например, на это графике видно, какую производительность выдает некое серверное приложение без использования async/await и с использованием async/await. Оба приложения ограничены 50-ю потоками:
Видно, что как только начинает приходить больше 50-ти одновременных запросов, синхронное приложение начинает отвечать хуже, потому что потоки по большей части заняты бесполезным ожиданием. Асинхронное же приложение продолжает нормально отвечать на запросы, потому что потоки все время работают и даже 50-ти потоков хватает, чтобы обслужить 100 клиентов без потери во времени отклика. Представьте себе аналогию: ресторан -- это ваше приложение, официанты -- это потоки в приложении, клиент за столиком -- это запрос. В случае синхронного приложение происходит вот что: Клиент садится за столик, ему приносит меню официант (пришел новый запрос, поток занялся его обработкой) Клиент листает меню и думает, что же ему выбрать официант стоит рядом и ждет, пока клиент сделает заказ (началась IO операция, поток блокировался) Заказ сделан, официант несет его на кухню и ждет приготовления заказа (началась другая IO операция, поток снова блокировался) Заказ готов, официант несет его клиенту, клиент начинает есть, а официант стоит рядом и ждет, когда можно будет унести пустую посуду (началась третья IO операция, поток снова простаивает в ожидании) Фактически получается та же самая ситуация -- на каждого клиента нужен свой официант. Абсурд! Вы тратите лишние деньги на зарплату людям, которые бОльшую часть времени ничего не делают. Точно так же ОС тратит лишние ресурсы на потоки, которые блокируются в ожидании. В правильном же ресторане официант занят только на подаче меню, приеме заказа, подаче блюда и уборке. В остальное время он не простаивает в ожидании, а обслуживает других клиентов. Например, один официант может одновременно обслуживать пять столиков. Так и в асинхронном приложении небольшое количество потоков обслуживает большое количество запросов. Ключевые слова async и await Да, async и await являются всего лишь ключевыми словами в языке, т.е. по сути служат всего лишь некоторыми указаниями для компилятора. Ключевое слово async делает три вещи: разрешает использование ключевого слова await "передает" результат выполнения метода или возникшее исключение вверх по стеку говорит компилятору о том, что данный метод нужно специальным образом скомпилировать -- превратить в стейт-машину Ключевое слово await делает две вещи: указывает точку возможного прерывания/возобновления метода возможного -- потому что если таск уже завершен, то метод продолжит выполнение и прерываться не будет извлекает результат или исключение из таска, который возвращается ожидаемым методом Т.е. никакого отношения к потокам эти два слова не имеют. Для более детального ликбеза могу посоветовать вам посмотреть вот это выступление или хотя бы слайды, где (я надеюсь :)) доступно и на пальцах изложено, как работает async/await, а также разобраны основные заблуждения (коими и наполнен вопрос). Что происходит в приведенном методе? Сперва выполняет часть метода до первого await: Random r = new Random() while (howmuch-- > 0) { await Task.Delay(1000 * r.Next(1, 3)) Затем начинает ожидание, а текущий поток покидает метод и используется CLR для чего-то другого. Если это был UI поток, то он пойдет обрабатывать message loop. Ожидание завершается. Следующая часть кода начинает выполняться в том же контексте, в котором выполнялась предыдущая часть. Это значит, что если до этого у нас был UI контекст, ASP.NET контекст, или любой другой однопоточный контекст, то продолжение будет выполнено в том же потоке, что и предыдущая часть. Если же код выполнялся в многопоточном контексте (например, в пуле потоков), то тут уже гарантий никаких нет -- это может быть тот же поток, а может быть и нет: var v = string.Format("automatic {0}", r.Next(1, 10)) await queue.SendAsync(v) Как только метод SendAsync() внутри себя примет запрос, он вернет управление, наш поток в свою очередь снова выйдет из текущего метода. Через некоторое время метод SendAsync() завершится, и наш метод снова продолжит работу.
Теперь давайте пройдемся по конкретным вопросам: Непонимаю для чего надо вызывать await Task.Delay? Про то, что это для иммитации бурной деятелности это понятно. Этот вопрос на самом деле надо задавать автору кода. Возможно, имитация, а возможно и искусственное ограничение, чтобы на учебном примере было видно, как работают производитель и потребитель. Вопрос о другом: если надо остановить текущий поток, то для чего запускать другой поток? Почему не сделано просто: Task.Delay(1000 * r.Next(1, 3)).Wait() ? Предложенная вами реализация будет занимать поток. Поток 1-3 секунды не будет занят ничем полезным, кроме ожидания. При использовании await поток на время ожидания будет свободен и может быть использован для другой работы. А когда время ожидания истечет (ожидание при этом хитрым образом делается на уровне CLR/ОС с использованием системных таймеров и ресурсов практически не требует), то выполнение метода будет продолжено. И, как уже было сказано выше, не факт, что в "другом потоке". Если вызвали Delay, то значит надо подождать указанное время, то есть надо остановить текущий поток. Как я уже сказал, текущий поток останавливать совсем не обязательно. Task.Delay() как раз реализует такое ожидание, которое не требует занятия отдельного потока. А текущий поток в это время может заняться другой работой. А из-за await получается какая-то ерунда, так как поток вызывает другой поток, чтобы ждать в нем, и сам ждет другой поток. Без комментариев :). Если вы прочитали и поняли все, что я уже написал выше, а тем более посмотрели видео, то отвечать на это подробно уже нет нужды. Сами сможете ответить. Если метод возвращает Task, то почему нет return? Как я уже сказал выше, модификатор async в т.ч. говорит компилятору, что данный метод нужно специальным образом скомпилировать. Побочным эффектом специальной компиляции и является тот факт, что вместо Task можно возвращать void, а вместо Task -- T. Кажется, async и await лишние. Если делать все правильно, то нет, не лишние.