Подтвердить что ты не робот

Почему веб-приложения сходят с ума с await/async в настоящее время?

Я исхожу из заднего фона/толстого клиентского фона, поэтому, возможно, я чего-то не хватает... но недавно я посмотрел на источник для токенов-сервера с открытым исходным кодом JWT, и авторы сошли с ума с ожиданием/асинхронным. Как для каждого метода, так и для каждой строки.

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

Это чрезмерное использование await/async для веб-разработчика или для чего-то вроде Angular? Это было на маркерном сервере JWT, поэтому даже не видя, что он должен делать с любым из них. Это просто конечная точка REST.

Как сделать каждую асинхронную линию для улучшения производительности? Для меня это убьет производительность от вращения всех этих потоков, нет?

4b9b3361

Ответ 1

Я получаю то, что шаблон для... для запуска длинных запущенных задач в отдельном потоке.

Это абсолютно не то, что этот шаблон для.

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

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

Скручивание нового потока похоже на наем рабочего. Когда вы ждете задания, вы не нанимаете рабочего для выполнения этой задачи. Вы спрашиваете: "Эта задача уже выполнена? Если нет, перезвоните мне, когда ее сделают, чтобы я мог продолжать выполнять работу, которая зависит от этой задачи. В то же время я собираюсь поработать над этим другим делом здесь.."

Если вы делаете свои налоги, и вы обнаружите, что вам нужен номер из вашей работы, а почта еще не пришла, вы не нанимаете рабочего, чтобы ждать по почтовому ящику. Вы отмечаете, где вы были в своих налогах, идите за другим, и когда приходит почта, вы забираете место, где вы остановились. Это ждет. Он асинхронно ждет результата.

Это чрезмерное использование await/async для веб-разработчика или для чего-то вроде Angular?

Для управления задержкой.

Как сделать каждую асинхронную линию для повышения производительности?

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

Для меня это убьет производительность от разворота всех этих потоков, нет?

Нет нитей. Concurrency - механизм достижения асинхронности; это не единственный.

Хорошо, поэтому, если я пишу код вроде: ждут someMethod1(); дождаться someMethod2(); дождаться someMethod3(); что волшебно собирается сделать приложение более отзывчивым?

Более отзывчивый по сравнению с чем? По сравнению с вызовами этих методов, не ожидая их? Нет, конечно нет. По сравнению с синхронным ожиданием завершения задач? Абсолютно, да.

То, что я не получаю, я думаю. Если вы ожидаете на всех 3 в конце, тогда да, вы параллельно используете 3 метода.

Нет, нет. Перестаньте думать о parallelism. Там не должно быть никаких parallelism.

Подумайте об этом так. Вы хотите приготовить жареный яичный бутерброд. У вас есть следующие задачи:

  • Жарьте яйцо
  • Тост хлеба
  • Соберите сэндвич

Три задачи. Третья задача зависит от результатов первых двух, но первые две задачи не зависят друг от друга. Итак, вот несколько рабочих процессов:

  • Положите яйцо в кастрюлю. Пока яйцо жарится, смотрите на яйцо.
  • Как только яйцо закончено, положите тост в тостер. Посмотрите на тостер.
  • Как только тост будет сделан, положите яйцо на тост.

Проблема в том, что вы могли бы поставить тост в тостер, пока яйцо готовит. Альтернативный рабочий процесс:

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

Вы видите, почему асинхронный рабочий процесс намного эффективнее? Вы получаете много материала, пока ожидаете завершения операции с высокой задержкой. Но вы не наняли яичного шеф-повара и шеф-повара. Нет новых тем!

Предлагаемый рабочий процесс:

eggtask = FryEggAsync();
toasttask = MakeToastAsync();
egg = await eggtask;
toast = await toasttask;
return MakeSandwich(egg, toast);

Теперь сравните это с:

eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
return MakeSandwich(egg, toast);

Вы видите, как этот рабочий процесс отличается? Этот рабочий процесс:

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

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

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

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

Мне сложно понять, как возможно асинхронное программирование без использования parallelism где-то. Например, как вы скажете программе начать работу над тостом, ожидая, пока яйца без DoEggs() будут работать одновременно, по крайней мере, внутри?

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

Как вы это сделали? Разбивая задачи на мелкие кусочки, отмечая, какие части должны быть выполнены в каком порядке, а затем совместно многозадачные фрагменты.

Сегодня дети с их большими плоскими виртуальными моделями памяти и многопоточными процессами считают, что так оно и было, но моя память простирается до дней Windows 3, у которых ничего не было. Если вы хотите, чтобы две вещи происходили "параллельно", что вы делали: разбивали задачи на мелкие части и по очереди исполняли детали. Вся эта операционная система была основана на этой концепции.

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

Если вы хотите получить более подробное объяснение того, как аппаратное обеспечение работает с операционной системой для достижения асинхронности, прочитайте "Нет потока" Стивена Клири.

Итак, когда вы видите "асинхронный", не думайте "параллельно". Подумайте, что операция с высокой латентностью разделена на мелкие кусочки. Если существует много таких операций, части которых не зависят друг от друга, тогда вы можете совместно перемежать выполнение этих фрагментов в одном потоке.

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

ОБНОВЛЕНИЕ:

В последнем примере, какая разница между


eggtask = FryEggAsync(); 
egg = await eggtask; 
toasttask = MakeToastAsync(); 
toast = await toasttask; 

egg = await FryEggAsync(); 
toast = await MakeToastAsync();?

Я предполагаю, что он называет их синхронно, но выполняет их асинхронно? Я должен признать, что я даже не потрудился ждать задания отдельно раньше.

Нет никакой разницы.

Когда вызывается FryEggAsync, он называется, независимо от того, появляется ли оно await перед ним или нет. await - оператор . Он работает с вещью , возвращенной из вызова FryEggAsync. Это как и любой другой оператор.

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

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

Итак, да,

var x = Foo();
var y = await x;

и

var y = await Foo();

- это то же самое, что и

var x = Foo();
var y = 1 + x;

и

var y = 1 + Foo();

- одно и то же.

Итак, пропустите это еще раз, потому что вы, кажется, верите в миф, что await вызывает асинхронность. Это не так.

async Task M() { 
   var eggtask = FryEggAsync(); 

Предположим, что M() вызывается. FryEggAsync. Синхронно. Асинхронного вызова нет; вы видите вызов, контроль переходит к вызываемой стороне, пока вызывающая сторона не вернется. Вызов возвращает задачу, которая представляет собой яйцо, которое будет доступно в будущем.

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

  egg = await eggtask; 

Теперь мы берем эту задачу, и await спрашивает ее: "Вы закончили?" Если ответ "да", то egg присваивается значение, созданное задачей. Если ответ отсутствует, то M() возвращает a Task, представляющий "работа M будет завершена в будущем". Остальная часть M() записывается как продолжение eggtask, поэтому, когда eggtask завершается, он снова вызовет M() и заберет его не с самого начала, а из назначения в egg. M() является возобновляемым при любом точечном методе. Компилятор делает необходимую магию, чтобы это произошло.

Итак, теперь мы вернулись. Нить продолжает делать то, что она делает. В какой-то момент яйцо готово, поэтому вызывается продолжение eggtask, что вызывает вызов M() снова. Он возобновляется в тот момент, когда он был остановлен: присвоение только что произведенного яйца egg. И теперь мы продолжаем грузоперевозки:

toasttask = MakeToastAsync(); 

Опять же, вызов возвращает задачу, и мы:

toast = await toasttask; 

проверьте, завершена ли задача. Если да, мы назначаем toast. Если нет, то мы снова возвращаемся из M(), а продолжение toasttask является * остатком M().

И так далее.

Устранение переменных Task не имеет ничего общего. Выделяется хранилище для значений; ему просто не дано имя.

ДРУГОЕ ОБНОВЛЕНИЕ:

существует ли случай, когда нужно как можно раньше вызвать методы возвращения задачи, но как можно скорее подождать их?

Приведенный пример:

var task = FooAsync();
DoSomethingElse();
var foo = await task;
...

Для этого есть какой-то случай. Но позвольте сделать шаг назад. Целью оператора await является построение асинхронного рабочего процесса с использованием соглашений о кодировании синхронного рабочего процесса. Так что думать о том, что это за рабочий процесс? Рабочий процесс налагает порядок на набор связанных задач.

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

Вы также можете представлять зависимости от побочных эффектов. Например, пользователь нажимает кнопку, поэтому вы хотите воспроизвести звук сирены, затем подождите три секунды, затем откройте дверь, затем подождите три секунды, затем закройте дверь:

DisableButton();
PlaySiren();
await Task.Delay(3000);
OpenDoor();
await Task.Delay(3000);
CloseDoor();
EnableButton();

Было бы бессмысленно говорить

DisableButton();
PlaySiren();
var delay1 = Task.Delay(3000);
OpenDoor();
var delay2 = Task.Delay(3000);
CloseDoor();
EnableButton();
await delay1;
await delay2;

Потому что это не желаемый рабочий процесс.

Итак, фактический ответ на ваш вопрос: отложить ожидание до тех пор, пока точка, где это действительно необходимо, является довольно хорошей практикой, поскольку она увеличивает возможности для эффективной работы графика. Но вы можете зайти слишком далеко; убедитесь, что рабочий процесс, который был реализован, - это требуемый рабочий процесс.

Ответ 2

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

Когда вы конвертируете синхронный код в асинхронный код, вы обнаружите, что он работает лучше всего, если асинхронный вызов кода и вызывается другим асинхронным кодом - вплоть до (или "вверх", если хотите). Другие также заметили распространение асинхронного программирования и назвали его "заразным" или сравнили его с вирусом зомби. Независимо от того, черепахи или зомби, определенно верно, что асинхронный код имеет тенденцию приводить окружающий код в асинхронный. Это поведение присуще всем типам асинхронного программирования, а не только новым ключевым словам async/await.

Источник: Async/Await - лучшие практики в асинхронном программировании

Ответ 3

Это мир актеров-моделей, действительно...

Мое мнение состоит в том, что async/await - это просто способ переопределения программных систем, чтобы не допустить, чтобы на самом деле многие системы (особенно те, у которых много сетевых коммуникаций) лучше воспринимались как модель Actor (или, еще лучше, коммуникационных процессов).

В обоих случаях все дело в том, что вы ожидаете, что одна из нескольких вещей станет полной, предпримите необходимые действия, когда это произойдет, а затем вернитесь к ожиданию. В частности, вы ожидаете, что сообщение прибудет откуда-то еще, прочитав его и действуя на контент. В * nix ожидание обычно выполняется с вызовом epoll() или select().

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

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

Как Windows сильно затрудняет работу

К сожалению, Windows очень затрудняет внедрение системы proactor ( "если вы прочтете это сообщение сейчас, вы его получите" ). Windows - это реактор ( "это сообщение, которое вы просили меня прочитать? Теперь его прочитали" ). Это важное различие.

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

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

Я в какой-то степени разбираюсь. Proactor очень полезен в системах с динамической связностью, Актеры опускаются в систему, выпадающие снова. Реактор в порядке, если у вас есть постоянное население актеров с коммуникационными ссылками, которые никогда не исчезнут. Тем не менее, учитывая, что реакторная система легко внедряется на платформе проактора, но проакторная система не может быть легко реализована на платформе реактора (время не вернется назад), я считаю, что подход Окна особенно раздражает.

Так или иначе, async/await, безусловно, все еще находятся в зоне реактора.

Удар по удару

Это заразило многие другие библиотеки.

С++ Boost asio также является реактором даже на * nix, в основном это кажется потому, что они хотели иметь реализацию Windows.

ZeroMQ, который является основой proactor, в некоторой степени ограничен Windows, основанный на вызове select() (который в Windows работает только с сокетами).

Для семейства Cygwin времени выполнения POSIX в Windows им пришлось реализовать select(), epoll() и т.д., указав поток на запрос дескриптора файла (да, опрос!!!! ) базовый сокет/последовательный порт/канал для входящих данных для воссоздания подпрограмм POSIX. Yeurk! Комментарии к спискам рассылки cygwin dev, относящимся ко времени, когда они выполняли эту часть, делают забавное чтение.

Актер не обязательно медленно

Стоит отметить, что фраза "передача сообщений" не обязательно означает передачу копий вокруг - существует множество формулировок модели Actor, где вы просто передаете право собственности на ссылки на сообщения (например, Dataflow, часть задачи Параллельная библиотека в С#). Это делает его быстрым. Я еще не обошел библиотеку Dataflow, но на самом деле это не делает Windows proactor неожиданным. Это не дает вам систему проактора модели актера, работающую на всех типах носителей данных, таких как сокеты, каналы, очереди и т.д.

Windows 10 Linux Runtime

Таким образом, только что взорвав Windows и низкую архитектуру реактора, одним из интригующих моментов является то, что Windows 10 теперь использует двоичные файлы Linux. Как мне хотелось бы узнать, Microsoft внедрила системный вызов, который лежит в основе select(), epoll(), учитывая, что он должен функционировать на сокетах, последовательных портах, каналах и всего остального в стране POSIX, которая является файловый дескриптор, когда все остальное на Windows не может? Я бы дал свои задние зубы знать ответ на этот вопрос.