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