Я не нашел много ресурсов по этому поводу: мне было интересно, если это возможно / хорошая идея, чтобы иметь возможность писать асинхронный код синхронным способом.
Например, вот некоторый код JavaScript, который извлекает количество пользователей, хранящихся в базе данных (асинхронная операция):
getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });
Было бы неплохо иметь возможность написать что-то вроде этого:
const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);
И поэтому компилятор автоматически позаботится о том, чтобы дождаться ответа и затем выполнить console.log
. Он всегда будет ждать завершения асинхронных операций, прежде чем результаты будут использованы где-либо еще. Мы бы намного меньше использовали обещания обратных вызовов, async / await или что-то еще, и никогда бы не волновались, доступен ли результат операции немедленно или нет.
Ошибки по-прежнему можно было бы обработать ( nbOfUsers
получили целое число или ошибку?), Используя try / catch, или что-то наподобие опций, как в языке Swift .
Является ли это возможным? Это может быть ужасная идея / утопия ... Я не знаю.
await
sa,Task<T>
чтобы преобразовать его вT
async
/await
вместо этого, что делает асинхронные части выполнения явными.Ответы:
Async / await - это именно то автоматизированное управление, которое вы предлагаете, хотя и с двумя дополнительными ключевыми словами. Почему они важны? Помимо обратной совместимости?
Без явных моментов, когда сопрограмма может быть приостановлена и возобновлена, нам потребуется система типов, чтобы определить, где следует ожидать ожидаемое значение. Многие языки программирования не имеют такой системы типов.
Делая ожидание значения явным, мы также можем передавать ожидаемые значения как объекты первого класса: обещания. Это может быть очень полезно при написании кода более высокого порядка.
Асинхронный код имеет очень глубокие последствия для модели исполнения языка, аналогично отсутствию или наличию исключений в языке. В частности, асинхронную функцию можно ожидать только от асинхронных функций. Это влияет на все вызывающие функции! Но что, если мы изменим функцию с не асинхронной на асинхронную в конце этой цепочки зависимостей? Это было бы обратно несовместимым изменением… если только все функции не являются асинхронными, и каждый вызов функции ожидается по умолчанию.
И это крайне нежелательно, поскольку оно имеет очень плохие последствия для производительности. Вы не сможете просто вернуть дешевые значения. Каждый вызов функции станет намного дороже.
Асинхронность великолепна, но некая неявная асинхронность не будет работать в реальности.
Чистые функциональные языки, такие как Haskell, имеют некоторый запасной штрих, потому что порядок выполнения в значительной степени не определен и ненаблюдаем. Или по-другому: любой конкретный порядок операций должен быть явно закодирован. Это может быть довольно громоздким для реальных программ, особенно для тех программ с интенсивным вводом / выводом, для которых асинхронный код очень хорошо подходит.
источник
someValue ifItIsAFuture [self| self messageIWantToSend]
потому, что это сложно интегрировать с общим кодом.par
любом месте бросить чистый код на Haskell и получить паралеллизм бесплатно.Чего вам не хватает, так это цели асинхронных операций: они позволяют вам использовать время ожидания!
Если вы превратите асинхронную операцию, например, запрос какого-либо ресурса от сервера, в синхронную операцию, неявно и немедленно ожидая ответа, ваш поток не сможет ничего сделать со временем ожидания . Если на ответ сервера требуется 10 миллисекунд, то теряется около 30 миллионов циклов ЦП. Задержка ответа становится временем выполнения запроса.
Единственная причина, по которой программисты изобрели асинхронные операции, заключается в том, чтобы скрыть задержку по своей сути долго выполняющихся задач за другими полезными вычислениями . Если вы можете заполнить время ожидания полезной работой, это сэкономит процессорное время. Если вы не можете, ну, ничего не потеряно из-за асинхронной операции.
Поэтому я рекомендую использовать асинхронные операции, которые вам предоставляют ваши языки. Они там, чтобы сэкономить ваше время.
источник
Некоторые делают.
Они не являются мейнстримом (пока), потому что async - это относительно новая функция, которую мы только сейчас почувствовали, если это даже хорошая функция, или как представить ее программистам так, чтобы она была дружественной / удобной в использовании / выразительным / и т.д.. Существующие асинхронные функции в значительной степени привязаны к существующим языкам, которые требуют немного другого подхода к проектированию.
Тем не менее, это не ясно хорошая идея , чтобы сделать везде. Распространенным сбоем является выполнение асинхронных вызовов в цикле, эффективно сериализующих их выполнение. Наличие неявных асинхронных вызовов может скрыть такую ошибку. Кроме того, если вы поддерживаете неявное принуждение от
Task<T>
(или эквивалента вашего языка) кT
, это может добавить немного сложности / затрат вашему контролеру типов и сообщениям об ошибках, когда неясно, какой из двух программист действительно хотел.Но это не непреодолимые проблемы. Если бы вы хотели поддержать такое поведение, вы почти наверняка могли бы, хотя были бы компромиссы.
источник
Есть языки, которые делают это. Но на самом деле в этом нет особой необходимости, поскольку это может быть легко достигнуто с помощью существующих языковых функций.
Пока у вас есть какой-то способ выражения асинхронности, вы можете реализовывать Futures или Promises исключительно как библиотечную функцию, вам не нужны никакие специальные языковые функции. И пока у вас есть несколько экспресс- прокси , вы можете объединить две функции и получить прозрачные фьючерсы .
Например, в Smalltalk и его потомках объект может изменить свою идентичность, он может буквально «стать» другим объектом (и фактически вызывается метод, который делает это
Object>>become:
).Представьте себе длительное вычисление, которое возвращает
Future<Int>
. ЭтоFuture<Int>
все те же методыInt
, что и в других реализациях.Future<Int>
«S+
метод не добавляет другой номер и возвращает результат, она возвращает новый ,Future<Int>
который оборачивает вычисления. И так далее. Методы, которые не могут быть разумным образом реализованы путем возврата aFuture<Int>
, вместо этого автоматически получатawait
результат, а затем вызовутself become: result.
, что сделает текущий исполняемый объект (self
т. Е.Future<Int>
) Буквально станетresult
объектом, то есть отныне ссылка на объект, которая раньше былаFuture<Int>
is теперьInt
везде, полностью прозрачно для клиента.Никаких специальных языковых возможностей, связанных с асинхронностью, не требуется.
источник
Future<T>
и другоеT
совместно использует какой-то общий интерфейс, и я использую функциональность этого интерфейса. Должен ли это бытьbecome
результатом, а затем использовать функциональность или нет? Я думаю о таких вещах, как оператор равенства или представление отладки в строку.a + b
, как делать , оба целых числа, не имеет значения, если a и b доступны сразу или позже, мы просто пишемa + b
(делая возможным сделатьInt + Future<Int>
)Future<T>
иT
потому что с вашей точки зрения, нетFuture<T>
, только aT
. Конечно, существует множество технических проблем, связанных с тем, как сделать это эффективным, какие операции должны быть блокирующими, а не неблокирующими и т. Д., Но это действительно не зависит от того, делаете ли вы это как язык или как библиотечную функцию. Прозрачность была требованием, оговоренным ОП в этом вопросе, я не буду спорить, что это сложно и может не иметь смысла.Они делают (ну, большинство из них). Функция, которую вы ищете, называется потоками .
Тем не менее, у потоков есть свои проблемы:
Поскольку код может быть приостановлен в любой момент , вы никогда не сможете предположить, что вещи не изменятся «сами по себе». При программировании с потоками вы тратите много времени на размышления о том, как ваша программа должна справляться с изменениями.
Представьте, что игровой сервер обрабатывает атаку игрока на другого игрока. Что-то вроде этого:
Три месяца спустя, игрок обнаруживает, что, будучи убитым и выйдя из системы точно во время
attacker.addInventoryItems
работы, затемvictim.removeInventoryItems
потерпит неудачу, он может оставить свои предметы, а злоумышленник также получит копию своих предметов. Он делает это несколько раз, добывая миллион тонн золота из воздуха и разрушая экономику игры.Кроме того, злоумышленник может выйти из системы, пока игра отправляет сообщение жертве, и у него над головой не будет метки «убийца», поэтому его следующая жертва не убежит от него.
Поскольку код может быть приостановлен в любой момент , вам нужно везде использовать блокировки при манипулировании структурами данных. Я привел приведенный выше пример, который имеет очевидные последствия в игре, но он может быть более тонким. Рассмотрите возможность добавления элемента в начало связанного списка:
Это не проблема, если вы говорите, что потоки могут быть приостановлены только тогда, когда они выполняют ввод-вывод, а не в любой момент. Но я уверен, что вы можете представить себе ситуацию, когда есть операция ввода-вывода, например, регистрация:
Поскольку код может быть приостановлен в любой момент , потенциально может быть много состояний для сохранения. Система справляется с этим, предоставляя каждому потоку совершенно отдельный стек. Но стек довольно большой, поэтому в 32-битной программе не может быть более 2000 потоков. Или вы можете уменьшить размер стека, рискуя сделать его слишком маленьким.
источник
Здесь многие ответы вводят в заблуждение, потому что, хотя вопрос буквально задавался об асинхронном программировании, а не неблокирующем вводе-выводе, я не думаю, что мы можем обсуждать одно без обсуждения другого в данном конкретном случае.
Хотя асинхронное программирование, по сути, асинхронно, смыслом асинхронного программирования в основном является предотвращение блокировки потоков ядра. Node.js использует асинхронность через обратные вызовы или
Promise
s, чтобы разрешить отправку блокирующих операций из цикла событий, а Netty в Java использует асинхронность через обратные вызовы илиCompletableFuture
s, чтобы сделать что-то подобное.Неблокирующий код не требует асинхронности, однако . Это зависит от того, сколько ваш язык программирования и среда выполнения готовы сделать для вас.
Go, Erlang и Haskell / GHC могут с этим справиться. Вы можете написать что-то вроде этого
var response = http.get('example.com/test')
и сделать так, чтобы он за кулисами выпустил поток ядра, ожидая ответа. Это делается с помощью goroutines, процессов Erlang илиforkIO
потоков ядра за кулисами при блокировке, позволяя ему делать другие вещи в ожидании ответа.Это правда, что язык не может справиться с асинхронностью для вас, но некоторые абстракции позволяют вам идти дальше, чем другие, например, неограниченные продолжения или асимметричные сопрограммы. Тем не менее, основная причина асинхронного кода, блокировка системных вызовов, абсолютно может быть удалена от разработчика.
Node.js и Java поддерживают асинхронный неблокирующий код, тогда как Go и Erlang поддерживают синхронную неблокирующую код. Они оба действительные подходы с различными компромиссами.
Мой довольно субъективный аргумент состоит в том, что те, кто спорит против среды выполнения, управляющей неблокированием от имени разработчика, похожи на тех, кто спорил против сборки мусора в начале 90-х годов. Да, это сопряжено с затратами (в данном случае в основном с увеличением объема памяти), но упрощает разработку и отладку, а также повышает надежность кодовых баз.
Я лично утверждаю, что асинхронный неблокирующий код должен быть зарезервирован для системного программирования в будущем, а более современные технологические стеки должны перейти на синхронные неблокирующие среды выполнения для разработки приложений.
источник
waitpid(..., WNOHANG)
которая не работает, если ей придется блокировать. Или «синхронный» здесь означает «нет видимых программисту обратных вызовов / конечных автоматов / циклов событий»? Но для вашего примера Go мне все равно придется явно ожидать результата от goroutine, читая канал, не так ли? Как это менее асинхронно, чем async / await в JS / C # / Python?Если я правильно вас понял, вы запрашиваете модель синхронного программирования, но высокопроизводительную реализацию. Если это правильно, то это уже доступно нам в форме зеленых нитей или процессов, например, Erlang или Haskell. Так что да, это отличная идея, но адаптация к существующим языкам не всегда может быть настолько гладкой, как хотелось бы.
источник
Я ценю этот вопрос и считаю, что большинство ответов просто защищают статус-кво. В спектре языков низкого и высокого уровня мы застряли в колее на некоторое время. Следующим более высоким уровнем явно будет язык, который менее сфокусирован на синтаксисе (потребность в явных ключевых словах, таких как await и async) и гораздо больше на намерении. (Очевидный кредит Чарльзу Симони, но он думает о 2019 году и будущем.)
Если я сказал программисту, напишите какой-нибудь код, который просто извлекает значение из базы данных, вы можете смело предположить, что я имею в виду «и, кстати, не вешать интерфейс» и «не вводить другие соображения, которые маскируют трудно найти ошибки ». Программисты будущего с языками и инструментами следующего поколения, безусловно, смогут писать код, который просто выбирает значение в одной строке кода и уходит оттуда.
Язык самого высокого уровня будет говорить по-английски, и полагаться на компетентность лица, выполняющего задачу, чтобы знать, что вы действительно хотите сделать. (Подумайте о компьютере в Star Trek или спросите что-то об Alexa.) Мы далеки от этого, но приближаемся, и я ожидаю, что язык / компилятор может быть больше для генерации надежного, намеренного кода, не заходя так далеко, чтобы нуждающийся в ИИ.
С одной стороны, есть новые визуальные языки, такие как Scratch, которые делают это и не увязают во всех синтаксических технических особенностях. Конечно, за кулисами происходит много закулисной работы, поэтому программисту не о чем беспокоиться. Тем не менее, я не пишу программное обеспечение бизнес-класса на Scratch, поэтому, как и вы, у меня такое же ожидание, что зрелым языкам программирования пора автоматически управлять синхронной / асинхронной проблемой.
источник
Проблема, которую вы описываете, двоякая.
Есть несколько способов добиться этого, но они в основном сводятся к
foo(4, 7, bar, quux)
.Для (1) я собираю вместе ветвление и запуск нескольких процессов, порождаем несколько потоков ядра и реализации зеленых потоков, которые планируют потоки уровня времени выполнения для потоков ядра. С точки зрения проблемы они одинаковы. В этом мире ни одна функция никогда не сдается и не теряет контроль с точки зрения своего потока . Сам поток иногда не имеет контроля, а иногда не работает, но вы не отказываетесь от контроля над собственным потоком в этом мире. Система, подходящая для этой модели, может иметь или не иметь возможность создавать новые потоки или присоединяться к существующим. Система, подходящая для этой модели, может иметь или не иметь возможность дублировать поток, как в Unix
fork
.(2) интересно. Чтобы сделать это справедливо, нам нужно поговорить о формах введения и устранения.
Я собираюсь показать, почему неявное
await
не может быть добавлено к языку, подобному Javascript, обратно совместимым способом. Основная идея заключается в том, что, предоставляя пользователю обещания и различая синхронный и асинхронный контексты, в Javascript просочилась деталь реализации, которая препятствует равномерной обработке синхронных и асинхронных функций. Также существует тот факт, что вы не можетеawait
обещать вне тела асинхронной функции. Эти варианты дизайна несовместимы с «сделать асинхронность невидимой для вызывающего».Вы можете ввести синхронную функцию с помощью лямбда-выражения и устранить ее с помощью вызова функции.
Синхронное введение функции:
Синхронное исключение функций:
Вы можете противопоставить это введению и устранению асинхронных функций.
Внедрение асинхронной функции
Устранение асинхронной функции (примечание: действует только внутри
async
функции)Основная проблема здесь заключается в том, что асинхронная функция также является синхронной функцией, создающей объект обещания .
Вот пример вызова асинхронной функции синхронно в repl node.js.
Вы можете гипотетически иметь язык, даже динамически типизированный, где разница между асинхронными и синхронными вызовами функций не видна на сайте вызовов и, возможно, не видна на сайте определения.
Взяв такой язык и опуская его до Javascript, можно просто сделать все функции асинхронными.
источник
С помощью языковых процедур Go и времени выполнения языка Go вы можете писать весь код, как если бы он был синхронизированным. Если операция блокируется в одной процедуре, выполнение продолжается в других программах. И с помощью каналов вы можете легко общаться между Goroutines. Это часто проще, чем обратные вызовы, как в Javascript или async / await на других языках. Смотрите для https://tour.golang.org/concurrency/1 для некоторых примеров и объяснений.
Кроме того, у меня нет личного опыта с этим, но я слышал, что Эрланг имеет аналогичные возможности.
Итак, да, есть языки программирования, такие как Go и Erlang, которые решают проблему синхронной / асинхронной работы, но, к сожалению, они пока не очень популярны. По мере роста популярности этих языков, возможно, средства, которые они предоставляют, будут реализованы и на других языках.
источник
go ...
, так что он выглядит какawait ...
нет?go
. И практически любой вызов, который может быть заблокирован, выполняется асинхронно средой выполнения, которая тем временем просто переключается на другую программу (совместная многозадачность). Вы ждете, ожидая сообщения.await
чтению с канала<- ch
.Есть очень важный аспект, который еще не был поднят: повторный вход. Если у вас есть какой-либо другой код (т. Е. Цикл обработки событий), который выполняется во время асинхронного вызова (а если нет, то зачем вам вообще нужен асинхронный вызов?), То этот код может повлиять на состояние программы. Вы не можете скрыть асинхронные вызовы от вызывающего, потому что вызывающий может зависеть от частей состояния программы, чтобы оставаться неизменным в течение всего времени его вызова функции. Пример:
Если
bar()
это асинхронная функция, то может быть возможноobj.x
изменить ее во время выполнения. Это было бы довольно неожиданно без намека на то, что панель асинхронна и этот эффект возможен. Единственной альтернативой может быть подозрение, что каждая возможная функция / метод является асинхронной, повторная выборка и повторная проверка любого нелокального состояния после каждого вызова функции. Это склонно к тонким ошибкам и может даже не быть возможным, если некоторые нелокальные состояния выбираются через функции. Из-за этого программист должен знать, какая из функций может изменить состояние программы неожиданным образом:Теперь ясно видно, что
bar()
это асинхронная функция, и правильный способ ее обработки заключается в повторной проверке ожидаемого значенияobj.x
после этого и любые изменения, которые могли произойти.Как уже отмечалось в других ответах, чисто функциональные языки, такие как Haskell, могут полностью избежать этого эффекта, избегая необходимости какого-либо общего / глобального состояния. У меня нет большого опыта работы с функциональными языками, поэтому я, вероятно, склонен к этому, но я не думаю, что отсутствие глобального состояния является преимуществом при написании больших приложений.
источник
В случае с Javascript, который вы использовали в своем вопросе, есть важный момент, о котором следует помнить: Javascript является однопоточным, и порядок выполнения гарантируется, пока нет асинхронных вызовов.
Так что если у вас есть последовательность, как ваша:
Вам гарантировано, что ничего больше не будет выполнено за это время. Нет необходимости в замках или чем-то подобном.
Однако, если
getNbOfUsers
он асинхронный, то:означает, что в то время как
getNbOfUsers
выполнения промежуточный результат и другой код могут выполняться между ними. Это, в свою очередь, может потребовать некоторой блокировки в зависимости от того, что вы делаете.Поэтому полезно знать, когда вызов асинхронный, а когда нет, поскольку в некоторых ситуациях вам нужно будет принять дополнительные меры предосторожности, которые вам не понадобились бы, если вызов был синхронным.
источник
getNbOfUsers()
возвращает обещание. Но в этом и заключается смысл моего вопроса: почему мы должны явно записать его как асинхронный, компилятор может обнаружить его и обработать автоматически другим способом.Это доступно в C ++
std::async
начиная с C ++ 11.А с C ++ можно использовать 20 сопрограмм:
источник
await
(илиco_await
в данном случае) ключевое слово?