Почему языки программирования не управляют автоматически синхронной / асинхронной проблемой?


27

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

Например, вот некоторый код JavaScript, который извлекает количество пользователей, хранящихся в базе данных (асинхронная операция):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

Было бы неплохо иметь возможность написать что-то вроде этого:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

И поэтому компилятор автоматически позаботится о том, чтобы дождаться ответа и затем выполнить console.log. Он всегда будет ждать завершения асинхронных операций, прежде чем результаты будут использованы где-либо еще. Мы бы намного меньше использовали обещания обратных вызовов, async / await или что-то еще, и никогда бы не волновались, доступен ли результат операции немедленно или нет.

Ошибки по-прежнему можно было бы обработать ( nbOfUsersполучили целое число или ошибку?), Используя try / catch, или что-то наподобие опций, как в языке Swift .

Является ли это возможным? Это может быть ужасная идея / утопия ... Я не знаю.


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

4
@ JörgWMittag Я представляю себе гипотетический C #, который неявно awaitsa, Task<T>чтобы преобразовать его вT
Caleth

6
То, что вы предлагаете, не выполнимо. Компилятор не должен решать, хотите ли вы ждать результата или, возможно, запустить и забыть. Или запустить в фоновом режиме и ждать позже. Зачем себя так ограничивать?
странно

5
Да, это ужасная идея. Просто используйте async/ awaitвместо этого, что делает асинхронные части выполнения явными.
Берги

5
Когда вы говорите, что две вещи происходят одновременно, вы говорите, что это нормально, что эти вещи происходят в любом порядке. Если ваш код не может дать понять, какие изменения порядка не нарушат ожидания вашего кода, то он не сможет сделать их параллельными.
Роб

Ответы:


65

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

  • Без явных моментов, когда сопрограмма может быть приостановлена ​​и возобновлена, нам потребуется система типов, чтобы определить, где следует ожидать ожидаемое значение. Многие языки программирования не имеют такой системы типов.

  • Делая ожидание значения явным, мы также можем передавать ожидаемые значения как объекты первого класса: обещания. Это может быть очень полезно при написании кода более высокого порядка.

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

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

Асинхронность великолепна, но некая неявная асинхронность не будет работать в реальности.

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


2
Вам не обязательно нужна система типов. Прозрачные варианты будущего, например, ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph, могут быть легко реализованы без поддержки системы tyoe или языка. В Smalltalk и его потомках объект может прозрачно изменять свою идентичность, в ECMAScript он может прозрачно изменять свою форму. Это все, что вам нужно, чтобы сделать Futures прозрачным, нет необходимости в языковой поддержке для асинхронности.
Йорг Миттаг

6
@ JörgWMittag Я понимаю, что вы говорите и как это может работать, но прозрачные фьючерсы без системы типов затрудняют одновременное получение фьючерсов первого класса, не так ли? Мне нужно было бы каким-то образом выбрать, хочу ли я отправлять сообщения в будущее или в будущее значение, желательно что-то лучше, чем someValue ifItIsAFuture [self| self messageIWantToSend]потому, что это сложно интегрировать с общим кодом.
Амон

8
@amon "Я могу написать свой асинхронный код, поскольку обещания и обещания - это монады." Монады здесь на самом деле не нужны. Thunks - это просто обещания. Поскольку почти все значения в Haskell упакованы, почти все значения в Haskell уже являются обещаниями. Вот почему вы можете в parлюбом месте бросить чистый код на Haskell и получить паралеллизм бесплатно.
Дартфеннек

2
Async / await напоминает мне о продолжении монады.
ль

3
Фактически, оба исключения и async / await являются примерами алгебраических эффектов .
Алекс Рейкинг

21

Чего вам не хватает, так это цели асинхронных операций: они позволяют вам использовать время ожидания!

Если вы превратите асинхронную операцию, например, запрос какого-либо ресурса от сервера, в синхронную операцию, неявно и немедленно ожидая ответа, ваш поток не сможет ничего сделать со временем ожидания . Если на ответ сервера требуется 10 миллисекунд, то теряется около 30 миллионов циклов ЦП. Задержка ответа становится временем выполнения запроса.

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

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


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

6
@Cinn Я не нашел этого в вопросе, и пример в вопросе - Javascript, у которого нет этой функции. Однако, как правило, компилятору довольно сложно найти значимые возможности для распараллеливания, как вы описываете: осмысленное использование такой функции потребовало бы от программиста явного обдумывания того, что они исправили после долгого времени ожидания. Если вы сделаете среду выполнения достаточно умной, чтобы избежать этого требования для программиста, ваша среда выполнения, скорее всего, съест сбережения производительности, поскольку ей потребуется агрессивное распараллеливание между вызовами функций.
Мастер

2
Все компьютеры ждут с одинаковой скоростью.
Боб Джарвис - Восстановить Монику

2
@BobJarvis Да. Но они различаются по тому, сколько работы они могли бы сделать за время ожидания ...
cmaster

13

Некоторые делают.

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

Тем не менее, это не ясно хорошая идея , чтобы сделать везде. Распространенным сбоем является выполнение асинхронных вызовов в цикле, эффективно сериализующих их выполнение. Наличие неявных асинхронных вызовов может скрыть такую ​​ошибку. Кроме того, если вы поддерживаете неявное принуждение от Task<T>(или эквивалента вашего языка) к T, это может добавить немного сложности / затрат вашему контролеру типов и сообщениям об ошибках, когда неясно, какой из двух программист действительно хотел.

Но это не непреодолимые проблемы. Если бы вы хотели поддержать такое поведение, вы почти наверняка могли бы, хотя были бы компромиссы.


1
Я думаю, что идея может заключаться в том, чтобы обернуть все в асинхронные функции, синхронные задачи просто решатся немедленно, и мы получим один вид обработки (Edit: @amon объяснил, почему это плохая идея ...)
Cinn

8
Можете ли вы привести несколько примеров для " Некоторые делают ", пожалуйста?
Берги

2
Асинхронное программирование не является чем-то новым, просто сейчас людям приходится сталкиваться с ним чаще.
Куб

1
@ Кубик - насколько я знаю, это языковая особенность. Раньше это были просто (неловкие) пользовательские функции.
Теластин

12

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

Пока у вас есть какой-то способ выражения асинхронности, вы можете реализовывать Futures или Promises исключительно как библиотечную функцию, вам не нужны никакие специальные языковые функции. И пока у вас есть несколько экспресс- прокси , вы можете объединить две функции и получить прозрачные фьючерсы .

Например, в Smalltalk и его потомках объект может изменить свою идентичность, он может буквально «стать» другим объектом (и фактически вызывается метод, который делает это Object>>become:).

Представьте себе длительное вычисление, которое возвращает Future<Int>. Это Future<Int>все те же методы Int, что и в других реализациях.Future<Int>«S +метод не добавляет другой номер и возвращает результат, она возвращает новый , Future<Int>который оборачивает вычисления. И так далее. Методы, которые не могут быть разумным образом реализованы путем возврата a Future<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>)
Cinn

@Cinn: Да, вы можете сделать это с Transparent Futures, и вам не нужны никакие специальные языковые функции для этого. Вы можете реализовать его, используя уже существующие функции, например, Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript и, как я только что прочитал, Python.
Йорг Миттаг

3
@amon: Идея прозрачных фьючерсов заключается в том, что вы не знаете, что это будущее. С вашей точки зрения, нет единого интерфейса между, Future<T>и Tпотому что с вашей точки зрения, нетFuture<T> , только a T. Конечно, существует множество технических проблем, связанных с тем, как сделать это эффективным, какие операции должны быть блокирующими, а не неблокирующими и т. Д., Но это действительно не зависит от того, делаете ли вы это как язык или как библиотечную функцию. Прозрачность была требованием, оговоренным ОП в этом вопросе, я не буду спорить, что это сложно и может не иметь смысла.
Йорг Миттаг

3
@ Jörg Похоже, что это будет проблематично во всех, кроме функциональных языков, поскольку у вас нет возможности узнать, когда код фактически выполняется в этой модели. Это обычно хорошо работает, скажем, на Haskell, но я не могу понять, как это будет работать на более процедурных языках (и даже на Haskell, если вы заботитесь о производительности, вам иногда приходится форсировать выполнение и понимать основную модель). Тем не менее, интересная идея.
Во

7

Они делают (ну, большинство из них). Функция, которую вы ищете, называется потоками .

Тем не менее, у потоков есть свои проблемы:

  1. Поскольку код может быть приостановлен в любой момент , вы никогда не сможете предположить, что вещи не изменятся «сами по себе». При программировании с потоками вы тратите много времени на размышления о том, как ваша программа должна справляться с изменениями.

    Представьте, что игровой сервер обрабатывает атаку игрока на другого игрока. Что-то вроде этого:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Три месяца спустя, игрок обнаруживает, что, будучи убитым и выйдя из системы точно во время attacker.addInventoryItemsработы, затем victim.removeInventoryItemsпотерпит неудачу, он может оставить свои предметы, а злоумышленник также получит копию своих предметов. Он делает это несколько раз, добывая миллион тонн золота из воздуха и разрушая экономику игры.

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

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

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

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

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. Поскольку код может быть приостановлен в любой момент , потенциально может быть много состояний для сохранения. Система справляется с этим, предоставляя каждому потоку совершенно отдельный стек. Но стек довольно большой, поэтому в 32-битной программе не может быть более 2000 потоков. Или вы можете уменьшить размер стека, рискуя сделать его слишком маленьким.


3

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

Хотя асинхронное программирование, по сути, асинхронно, смыслом асинхронного программирования в основном является предотвращение блокировки потоков ядра. Node.js использует асинхронность через обратные вызовы или Promises, чтобы разрешить отправку блокирующих операций из цикла событий, а Netty в Java использует асинхронность через обратные вызовы или CompletableFutures, чтобы сделать что-то подобное.

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

Go, Erlang и Haskell / GHC могут с этим справиться. Вы можете написать что-то вроде этого var response = http.get('example.com/test')и сделать так, чтобы он за кулисами выпустил поток ядра, ожидая ответа. Это делается с помощью goroutines, процессов Erlang илиforkIO потоков ядра за кулисами при блокировке, позволяя ему делать другие вещи в ожидании ответа.

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

Node.js и Java поддерживают асинхронный неблокирующий код, тогда как Go и Erlang поддерживают синхронную неблокирующую код. Они оба действительные подходы с различными компромиссами.

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

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


1
Это был действительно интересный ответ! Но я не уверен, что понимаю ваше различие между «синхронным» и «асинхронным» неблокирующим кодом. Для меня синхронный неблокирующий код означает что-то вроде функции C, waitpid(..., WNOHANG)которая не работает, если ей придется блокировать. Или «синхронный» здесь означает «нет видимых программисту обратных вызовов / конечных автоматов / циклов событий»? Но для вашего примера Go мне все равно придется явно ожидать результата от goroutine, читая канал, не так ли? Как это менее асинхронно, чем async / await в JS / C # / Python?
Амон

1
Я использую «асинхронный» и «синхронный», чтобы обсудить модель программирования, предоставляемую разработчику, и «блокирование» и «неблокирование», чтобы обсудить блокировку потока ядра, во время которого он не может сделать ничего полезного, даже если есть другие вычисления, которые нужно выполнить, и есть запасной логический процессор, который он может использовать. Что ж, программа может просто ждать результата, не блокируя основной поток, но другая программа может общаться с ним по каналу, если пожелает. Однако программе не нужно напрямую использовать канал для ожидания чтения неблокирующего сокета.
Луи Джекман

Хм, хорошо, теперь я понимаю ваше различие. В то время как меня больше заботит управление потоком данных и контролем между сопрограммами, вы больше заботитесь о том, чтобы никогда не блокировать основной поток ядра. Я не уверен, что Go или Haskell имеют какое-либо преимущество перед C ++ или Java в этом отношении, поскольку они тоже могут запускать фоновые потоки, для этого просто требуется немного больше кода.
Амон

@LouisJackman может немного рассказать о вашем последнем утверждении об асинхронной неблокировке для системного программирования. Каковы плюсы асинхронного неблокирующего подхода?
солнечный день

@sunprophit Асинхронная неблокировка - это просто преобразование компилятора (обычно async / await), в то время как синхронная неблокировка требует поддержки во время выполнения, как некоторая комбинация сложных манипуляций со стеком, вставка точек текучести в вызовы функций (которые могут конфликтовать с встраиванием), отслеживание « сокращения »(требуется виртуальная машина, такая как BEAM) и т. д. Как и сборка мусора, она компенсирует меньшую сложность во время выполнения для простоты использования и надежности. Системные языки, такие как C, C ++ и Rust, избегают более крупных функций времени выполнения из-за своих целевых доменов, поэтому асинхронная неблокировка имеет больше смысла.
Луи Джекман

2

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


2

Я ценю этот вопрос и считаю, что большинство ответов просто защищают статус-кво. В спектре языков низкого и высокого уровня мы застряли в колее на некоторое время. Следующим более высоким уровнем явно будет язык, который менее сфокусирован на синтаксисе (потребность в явных ключевых словах, таких как await и async) и гораздо больше на намерении. (Очевидный кредит Чарльзу Симони, но он думает о 2019 году и будущем.)

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

Язык самого высокого уровня будет говорить по-английски, и полагаться на компетентность лица, выполняющего задачу, чтобы знать, что вы действительно хотите сделать. (Подумайте о компьютере в Star Trek или спросите что-то об Alexa.) Мы далеки от этого, но приближаемся, и я ожидаю, что язык / компилятор может быть больше для генерации надежного, намеренного кода, не заходя так далеко, чтобы нуждающийся в ИИ.

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


1

Проблема, которую вы описываете, двоякая.

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

Есть несколько способов добиться этого, но они в основном сводятся к

  1. иметь несколько потоков (на некотором уровне абстракции)
  2. иметь несколько видов функций на уровне языка, и все они называются так foo(4, 7, bar, quux).

Для (1) я собираю вместе ветвление и запуск нескольких процессов, порождаем несколько потоков ядра и реализации зеленых потоков, которые планируют потоки уровня времени выполнения для потоков ядра. С точки зрения проблемы они одинаковы. В этом мире ни одна функция никогда не сдается и не теряет контроль с точки зрения своего потока . Сам поток иногда не имеет контроля, а иногда не работает, но вы не отказываетесь от контроля над собственным потоком в этом мире. Система, подходящая для этой модели, может иметь или не иметь возможность создавать новые потоки или присоединяться к существующим. Система, подходящая для этой модели, может иметь или не иметь возможность дублировать поток, как в Unixfork .

(2) интересно. Чтобы сделать это справедливо, нам нужно поговорить о формах введения и устранения.

Я собираюсь показать, почему неявное awaitне может быть добавлено к языку, подобному Javascript, обратно совместимым способом. Основная идея заключается в том, что, предоставляя пользователю обещания и различая синхронный и асинхронный контексты, в Javascript просочилась деталь реализации, которая препятствует равномерной обработке синхронных и асинхронных функций. Также существует тот факт, что вы не можете awaitобещать вне тела асинхронной функции. Эти варианты дизайна несовместимы с «сделать асинхронность невидимой для вызывающего».

Вы можете ввести синхронную функцию с помощью лямбда-выражения и устранить ее с помощью вызова функции.

Синхронное введение функции:

((x) => {return x + x;})

Синхронное исключение функций:

f(4)

((x) => {return x + x;})(4)

Вы можете противопоставить это введению и устранению асинхронных функций.

Внедрение асинхронной функции

(async (x) => {return x + x;})

Устранение асинхронной функции (примечание: действует только внутри asyncфункции)

await (async (x) => {return x + x;})(4)

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

Вот пример вызова асинхронной функции синхронно в repl node.js.

> (async (x) => {return x + x;})(4)
Promise { 8 }

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

Взяв такой язык и опуская его до Javascript, можно просто сделать все функции асинхронными.


1

С помощью языковых процедур Go и времени выполнения языка Go вы можете писать весь код, как если бы он был синхронизированным. Если операция блокируется в одной процедуре, выполнение продолжается в других программах. И с помощью каналов вы можете легко общаться между Goroutines. Это часто проще, чем обратные вызовы, как в Javascript или async / await на других языках. Смотрите для https://tour.golang.org/concurrency/1 для некоторых примеров и объяснений.

Кроме того, у меня нет личного опыта с этим, но я слышал, что Эрланг имеет аналогичные возможности.

Итак, да, есть языки программирования, такие как Go и Erlang, которые решают проблему синхронной / асинхронной работы, но, к сожалению, они пока не очень популярны. По мере роста популярности этих языков, возможно, средства, которые они предоставляют, будут реализованы и на других языках.


Я почти никогда не использовал язык Go, но кажется, что вы явно заявляете go ..., так что он выглядит как await ...нет?
Цинн

1
@Cinn На самом деле нет. Вы можете поместить любой вызов в качестве goroutine на его собственное волокно / зеленую нить с помощью go. И практически любой вызов, который может быть заблокирован, выполняется асинхронно средой выполнения, которая тем временем просто переключается на другую программу (совместная многозадачность). Вы ждете, ожидая сообщения.
дедупликатор

2
Хотя подпрограммы являются своего рода параллелизмом, я бы не стал помещать их в тот же сегмент, что и async / await: не совместные сопрограммы, а автоматически (и с упреждением!) Запланированные зеленые потоки. Но это также не делает автоматическим ожидание: Go эквивалентно awaitчтению с канала <- ch.
Амон

@amon Насколько я знаю, подпрограммы планируются совместно на собственные потоки (обычно этого достаточно, чтобы максимально увеличить истинный аппаратный параллелизм) во время выполнения, а те предварительно планируются ОС.
дедупликатор

ОП попросил «иметь возможность писать асинхронный код синхронным способом». Как вы уже упоминали, с помощью goroutines и среды выполнения go вы можете это сделать. Вам не нужно беспокоиться о деталях многопоточности, просто пишите блокирующие чтения и записи, как если бы код был синхронным, и другие ваши программы, если таковые имеются, будут продолжать работать. Вам также даже не нужно «ждать» или читать с канала, чтобы получить эту выгоду. Поэтому я считаю, что Go - это язык программирования, наиболее точно соответствующий желаниям ОП.

1

Есть очень важный аспект, который еще не был поднят: повторный вход. Если у вас есть какой-либо другой код (т. Е. Цикл обработки событий), который выполняется во время асинхронного вызова (а если нет, то зачем вам вообще нужен асинхронный вызов?), То этот код может повлиять на состояние программы. Вы не можете скрыть асинхронные вызовы от вызывающего, потому что вызывающий может зависеть от частей состояния программы, чтобы оставаться неизменным в течение всего времени его вызова функции. Пример:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Если bar()это асинхронная функция, то может быть возможно obj.xизменить ее во время выполнения. Это было бы довольно неожиданно без намека на то, что панель асинхронна и этот эффект возможен. Единственной альтернативой может быть подозрение, что каждая возможная функция / метод является асинхронной, повторная выборка и повторная проверка любого нелокального состояния после каждого вызова функции. Это склонно к тонким ошибкам и может даже не быть возможным, если некоторые нелокальные состояния выбираются через функции. Из-за этого программист должен знать, какая из функций может изменить состояние программы неожиданным образом:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Теперь ясно видно, что bar()это асинхронная функция, и правильный способ ее обработки заключается в повторной проверке ожидаемого значенияobj.x после этого и любые изменения, которые могли произойти.

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


0

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

Так что если у вас есть последовательность, как ваша:

const nbOfUsers = getNbOfUsers();

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

Однако, если getNbOfUsersон асинхронный, то:

const nbOfUsers = await getNbOfUsers();

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

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


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

@Cinn это не моя точка зрения. Я хочу сказать, что поток выполнения может попасть в другие части вашего кода во время выполнения асинхронного вызова, в то время как для синхронного вызова это невозможно. Это все равно что запускать несколько потоков, но не знать об этом. Это может привести к большим проблемам (которые обычно трудно обнаружить и воспроизвести).
jcaron

-4

Это доступно в C ++ std::asyncначиная с C ++ 11.

Функция шаблона async запускает функцию f асинхронно (возможно, в отдельном потоке, который может быть частью пула потоков) и возвращает std :: future, который в конечном итоге будет содержать результат этого вызова функции.

А с C ++ можно использовать 20 сопрограмм:


5
Это не похоже на ответ на вопрос. По вашей ссылке: «Что дает нам Coroutines TS? Три новых языковых ключевых слова: co_await, co_yield и co_return» ... Но вопрос в том, зачем нам await(или co_awaitв данном случае) ключевое слово?
Артуро Торрес Санчес
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.