Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

Нужен async/await или не нужен?


Изучаю асинхронное программирование и вижу следующий метод 

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?
    


Ответы

Ответ 1



Зачем это нужно Давайте начнем с того, зачем вообще появилась нужда в 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 лишние. Если делать все правильно, то нет, не лишние.

Ответ 2



А из-за await получается какая-то ерунда, так как поток вызывает другой поток, чтобы ждать в нем, и сам ждет другой поток. Вы не совсем верно представляете себе механизм await. Ваш код вообще не обязательн запускает дополнительные потоки. Причина в том, что асинхронность и многопоточност - это две разные вещи. Например, javascript асинхронен - в нем все сетевые операции подразумевают callback по завершению. Но при этом он полностью однопоточен - код в нем выполняется без распараллеливания. Что реально происходит в вашем коде: Компилятор режет код на две части: Часть A: async Task Produce(ITargetBlock queue, int howmuch) { Random r = new Random(); while (howmuch-- > 0) { Часть B: var v = string.Format("automatic {0}", r.Next(1, 10)); await queue.SendAsync(v); } queue.Complete(); } Между ними находится какое-то долгое действие, результата которого можно ждать, н нагружая проц (по крайней мере в текущем потоке). В вашем случае это Task.Delay. В реальном случае это или долгая сетевая/дисковая операция, или явно запущенный в отдельном потоке код (Task.Run). Что происходит при выполнении этого кода, например, в WinForms: В вашем основном UI потоке выполняется часть А. В нем можно спокойно работать контролами, без всяких Invoke. Инициируется вызов долгой операции. Ваш основной поток выходит из метода. И спокойно занимается своим основным дело - отрисовывает контролы, обрабатывает клики - вобщем, приложение не замирает. Долгая операция завершается! Рантайм берет часть B и забрасывает ее на выполнение в основном потоке. В нем все так же можно спокойно работать с контролами, без всяких Invoke. Основной профит: Весь код метода выполняется в UI потоке, и для работы с контролами не нужны Invoke и прочая синхронизация, но при этом: Основной поток не стоит на месте, посредине метода, ожидая завершения долгой операции. UI живет, не подвисает. Попробуйте добиться того же без использования async/await. В случае ASP.NET нет UI потока, и профит от использования async/await заключаетс в освобождении потока на время ожидания долгой операции, что позволяет выполнять чуть больше одновременных запросов, не оставляя потоки висеть в ожидании.

Ответ 3



Кажется, async и await лишние если их убрать, то метод станет синхронным. Если вам нужен именно асинхронный метод, то async и await явно не лишние. Не понимаю для чего надо вызывать await Task.Delay? вероятно, это демонстрационный пример, и вызовом Delay имитируются некие долгие вычисления Если метод возвращает Task, то почему нет return? async/await методы, возвращающие Task, являются асинхронными аналогами для не асинхронных методов, возвращающих void. В них можно не указывать return явно. Почему не сделано просто: Task.Delay(1000 * r.Next(1, 3)).Wait();? потому что тогда никакой асинхронности не получится. Ваш метод в этом случае прост "повиснет" на период r.Next(1, 3) секунд, ожидая завершения Task.Delay, после чего продолжи выполнение. В приведенном же вами коде метод, достигнув await, приостановит выполнени до завершения Task.Delay и вернет управление вызвавшему его коду, который тем временем сможет заняться чем-то более полезным, чем ожидание. Когда задача завершится, будет выполнен "остаток" метода после Task.Delay - своеобразный коллбэк без явного указания функции обратного вызова. По приведённой @Grundy ссылке есть ответ Эрика Липперта с очень хорошей аналогией про официанта и заказ в ресторане, почитайте, многое встанет на свои места

Ответ 4



Давайте рассмотрим все по порядку, без лишних слов и так чтобы было понятно даж новичкам. 1) Метод скопирован из ответа VladD https://ru.stackoverflow.com/a/431145/201561 2) Метод используется в программе для TPL Dataflow (надстройка над Task'ами). 3) В методе присутствует цикл с ожиданием, то есть при каждом вызове цикла надо просто подождать, как это видно из следующего фрагмента метода. while (howmuch-- > 0) { await Task.Delay(1000 * r.Next(1, 3)); var v = ... В соседних ответах говорят, что когда поток доходит до await, то он освобождается для выполнения другой работы. Это правильно, но возникает вопрос: какой работы? Чтобы понять о какой другой работе идет речь, надо вспомнить о TaskScheduler и о Task'ах. Task'и можно сравнить с покупателями, которые стоят в очереди к кассам. В качестве кассира выступает Thread из пула потоков. TaskScheduler распределяет Task'и между кассами. Каждый Task дойдя до кассы отдает кассиру свой делегат. И Thread вызывает метод соответствующий делегату. В случае когда в методе указан await, то компилятор создает два делегата. То что находится в коде до await оказывается в делегате #1, а остальное - в делегате #2. Кассир вызывает делегат #1 и на этом его работа с этим Task заканчивается, и Thread освобождается для другой работы, со следующим Task. Что же проиходит с Delay и с делегатом #2? Происходит следующее: на какой-то другой свободной кассе вызывается Delay, что приводит к созданию еще одного Task, и запускается Timer. Через указанный промежуток времени Timer вызывает завершение Task, а затем вызывается делегат #2. Очевидно, что в данной ситуации нет необходимости в создании #1 и #2, также не нужно задействовать разные Thread. Вывод: если в текущем потоке надо просто подождать, то async/await не нужен.

Ответ 5



если коротко то без async не возможен await, а без await не возможен вызов асинхронной операции записи в очередь queue.SendAsync ради которой метод и написан. Можно использовать так же .Result у таск, и это избавит от await/async Но синхронизаци с вызывающим методом уже будет происходить по другому. В этом случае управление не будет передано и приемущество асинхронности будет лишь в том коде который между вызовом GetAsync и .Result. В вашем случае без await не произойдет перехода в другой поток и delay будет стопорить главный. Если есть синхронная копия queue.Send то можно воспользоваться ей. Но в этом случае не будет задержка изза синхронизации. Следует разлечать потоковые операции которые и так по своей природе асинхронные и операции с ресурсами, которые МОГУТ(а могут и нет) требовать тупого ожидания ответа ради них и был разработан механизм. До появления async приходилось пользоваться потоками или мудрить с колбеками для того чтобы разгрузить главный поток управления от ожиданий что невероятно усложняло код. В качестве иллюстрации код ниже при вызове в самом начале консольного приложения будет печатать букву x. ПРи этом будут выполняться все остальные команды приложения вврод выдор расчеты. Если убрать async/awaint очевидно что это вечный цикл и программа будет печатат букву и больше ничего не делать . async static Task Updater() { while (true) { await Task.Delay(100); Console.Write("x"); } }

Комментариев нет:

Отправить комментарий