Design: Предложение: Подождите

Созданный на 18 мая 2020  ·  96Комментарии  ·  Источник: WebAssembly/design

@rreverser и я хотели бы предложить новое предложение для WebAssembly: Await .

Мотивация предложения состоит в том, чтобы помочь « синхронному » коду, скомпилированному в WebAssembly, который делает что-то вроде чтения из файла:

fread(buffer, 1, num, file);
// the data is ready to be used right here, synchronously

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

const result = fetch("http://example.com/data.dat");
// result is a Promise; the data is not ready yet!

Другими словами, цель состоит в том, чтобы помочь с проблемой синхронизации/асинхронности , которая так часто встречается в wasm в Интернете.

Проблема синхронизации/асинхронности является серьезной проблемой. В то время как новый код может быть написан с учетом этого, большие существующие кодовые базы часто не могут быть рефакторингом, чтобы обойти это, а это означает, что они не могут работать в Интернете. У нас есть Asyncify , который обрабатывает файл wasm, позволяя приостанавливать и возобновлять работу, и который позволяет портировать некоторые такие кодовые базы, поэтому здесь мы не полностью заблокированы. Однако инструментирование wasm имеет значительные накладные расходы, что-то вроде увеличения размера кода на 50% и замедления в среднем на 50% (но иногда и намного хуже), потому что мы добавляем инструкции для записи/чтения обратно в локальное состояние и стек вызовов и так далее. Эти накладные расходы являются большим ограничением, и во многих случаях они исключают использование Asyncify!

Цель этого предложения — позволить приостановить и возобновить выполнение эффективным способом (в частности, без накладных расходов, как у Asyncify), чтобы все приложения, столкнувшиеся с проблемой синхронизации/асинхронности, могли легко ее избежать. Лично мы предназначаем это в первую очередь для Интернета, где это может помочь WebAssembly лучше интегрироваться с веб-API, но варианты использования за пределами Интернета также могут иметь значение.

Коротко об идее

Основная проблема здесь заключается в том, что код wasm является синхронным, а хост-среда — асинхронной. Поэтому наш подход сосредоточен на границе экземпляра wasm и снаружи. Концептуально, когда выполняется новая инструкция await , экземпляр wasm «ждет» чего-то извне. То, что означает «ожидание», будет отличаться на разных платформах и может не относиться ко всем платформам (например, не все платформы могут счесть релевантным предложение wasm atomics), но конкретно на веб-платформе экземпляр wasm будет ждать промиса и приостанавливаться. пока это не разрешится или не отклонится. Например, экземпляр wasm может приостановить сетевую операцию fetch и записать в .wat что-то вроде этого:

;; call an import which returns a promise
call $do_fetch
;; wait for the promise just pushed to the stack
await
;; do stuff with the result just pushed to the stack

Обратите внимание на общее сходство с await в JS и других языках. Хотя это не идентично им (подробности см. ниже), ключевым преимуществом является то, что он позволяет писать код, выглядящий синхронно (точнее, компилировать код, выглядящий синхронно, в wasm).

Детали

Основная спецификация wasm

Изменения в базовой спецификации wasm очень минимальны:

  • Добавьте тип waitref .
  • Добавьте инструкцию await .

Тип указывается для каждой инструкции await (например, call_indirect ), например:

;; elaborated wat from earlier, now with full types

(type $waitref_=>_i32 (func (param waitref) (result i32)))
(import "env" "do_fetch" (func $do_fetch (result waitref)))

;; call an import which returns a promise
call $do_fetch
;; wait for the promise just pushed to the stack
await (type $waitref_=>_i32)
;; do stuff with the result just pushed to the stack

Тип должен получить waitref и может возвращать любой тип (или ничего).

await определяется только с точки зрения того, что хост-среда должна что-то делать. В этом смысле она похожа на инструкцию unreachable , которая в Интернете заставляет хост выдать RuntimeError , но ее нет в базовой спецификации. Точно так же основная спецификация wasm говорит только о том, что await предназначено для ожидания чего-то от хост-среды, но не о том, как мы на самом деле это делаем, что может сильно отличаться в разных хост-средах.

Это основная спецификация wasm!

Васм JS спецификация

Изменения в спецификации wasm JS (которые затрагивают только среды JS, такие как Интернет) более интересны:

  • Допустимое значение waitref — это промис JS.
  • Когда await выполняется для промиса, весь экземпляр wasm приостанавливается и ждет разрешения или отклонения этого промиса.
  • Если обещание разрешается, экземпляр возобновляет выполнение после помещения в стек значения, полученного от обещания (если оно есть).
  • Если Promise отклоняется, мы возобновляем выполнение и выбрасываем исключение wasm из расположения await .

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

Как это выглядит для JS, когда он вызывает экземпляр wasm, который затем приостанавливается? Чтобы объяснить это, давайте сначала рассмотрим распространенный пример, возникающий при переносе нативных приложений на wasm, цикл событий:

void event_loop_iteration() {
  // ..
  while (auto task = getTask()) {
    task.run(); // this *may* be a network fetch
  }
  // ..
}

Представьте, что эта функция вызывается один раз за requestAnimationFrame . Он выполняет заданные ему задачи, которые могут включать в себя: рендеринг, физику, звук и выборку по сети. Если у нас есть событие выборки по сети, тогда и только тогда мы закончим выполнением инструкции await для промиса fetch . Мы можем сделать это 0 раз за один вызов event_loop_iteration , или 1 раз, или много раз. Мы знаем только, закончим ли мы это во время выполнения этого wasm, а не до и, в частности, не в JS-вызывающем объекте этого экспорта wasm. Таким образом, вызывающий объект должен быть готов к тому, что экземпляр либо приостановится, либо нет.

Несколько аналогичная ситуация может произойти и в чистом JavaScript:

function foo(bar) {
  // ..
  let result = bar(42);
  // ..
}

foo получает функцию JS bar и вызывает ее с некоторыми данными. В JS bar может быть асинхронной функцией или обычной. Если он асинхронный, он возвращает обещание и завершает выполнение только позже. Если это нормально, он выполняется перед возвратом и возвращает фактический результат. foo может либо предполагать, что знает тип bar (тип не написан в JS, на самом деле bar может даже не быть функцией!), либо может обрабатывать оба типа функций должны быть полностью общими.

Теперь, как правило, вы точно знаете, какой может быть набор функций bar ! Например, вы, возможно, написали foo и возможные bar s в координации или точно задокументировали ожидания. Но взаимодействие wasm/JS, о котором мы здесь говорим, на самом деле больше похоже на случай, когда у вас нет такой тесной связи между вещами, и где на самом деле вам нужно обрабатывать оба случая. Как упоминалось ранее, это требуется для примера event_loop_iteration . Но даже в более общем случае часто wasm — это ваше скомпилированное приложение, а JS — это общий код «времени выполнения», так что JS должен обрабатывать все случаи. Конечно, JS может легко это сделать, например, используя result instanceof Promise для проверки результата или используя JS await :

async function runEventLoopIteration() {
  // await in JavaScript can handle Promises as well as regular synchronous values
  // in the same way, so the log is guaranteed to be written out consistently after
  // the operation has finished (note: this handles 0 or 1 iterations, but could be
  // generalized)
  await wasm.event_loop_iteration();
  console.log("the event loop iteration is done");
}

(обратите внимание, что если нам не нужен этот console.log , то в этом примере нам не понадобится JS await , и будет просто обычный вызов wasm export)

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

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

Поддержка тулчейна/библиотеки

По нашему опыту работы с Asyncify и связанными инструментами легко (и весело!) написать небольшой JS для обработки ожидающего экземпляра wasm. Помимо опций, упомянутых ранее, библиотека может выполнять одно из следующих действий:

  1. Оберните экземпляр wasm, чтобы его экспорт всегда возвращал обещание. Это дает приятный простой внешний интерфейс (однако это добавляет накладные расходы на быстрые вызовы wasm, которые не приостанавливаются). Например, это то, что делает автономная вспомогательная библиотека Asyncify .
  2. Напишите некоторое глобальное состояние, когда экземпляр приостанавливается, и проверьте его из JS, который вызвал экземпляр. Это то, что делает, например, интеграция Emscripten Asyncify.

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

Реализация и производительность

Несколько факторов должны помочь сохранить простоту реализации виртуальных машин:

  1. Пауза/возобновление происходит только при ожидании, и мы знаем их местоположение статически внутри каждой функции.
  2. Когда мы возобновляем, мы продолжаем именно с того места, где остановились, и делаем это только один раз. В частности, мы никогда не "разветвляем" выполнение: здесь ничего не возвращается дважды, в отличие от C setjmp или сопрограммы в системе, которая допускает клонирование/разветвление.
  3. Допустимо, если скорость await медленнее, чем обычный вызов JS, так как мы будем ожидать промис, что как минимум подразумевает, что промис был выделен и что мы ждем в цикле событий ( который имеет минимальные накладные расходы плюс потенциальное ожидание других вещей, которые в настоящее время выполняются ). То есть варианты использования здесь не требуют, чтобы разработчики виртуальных машин находили способы сделать await молниеносно быстрыми. Мы хотим, чтобы await был эффективным только по сравнению с требованиями здесь, и, в частности, ожидаем, что он будет намного быстрее, чем большие накладные расходы Asyncify.

Учитывая вышеизложенное, естественной реализацией является копирование стека при паузе. Хотя это имеет некоторые накладные расходы, учитывая ожидаемую производительность здесь, это должно быть очень разумно. А если копировать стек только во время паузы, то можно избежать дополнительной работы по подготовке к паузе. То есть не должно быть дополнительных общих накладных расходов (что сильно отличается от Asyncify!)

Обратите внимание, что хотя копирование стека здесь является естественным подходом, это не совсем тривиальная операция, так как копирование может быть не простым memcpy, в зависимости от внутреннего устройства виртуальной машины. Например, если стек содержит указатели на себя, то их нужно либо скорректировать, либо сделать стек перемещаемым. В качестве альтернативы можно было бы скопировать стек обратно в его исходное положение перед его возобновлением, поскольку, как упоминалось ранее, он никогда не «разветвляется».

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

Нам очень интересно услышать отзывы разработчиков VM по этому разделу!

Уточнения

Это предложение только приостанавливает выполнение WebAssembly обратно вызывающему объекту экземпляра wasm. Он не позволяет приостанавливать кадры стека хоста (JS или браузера). await работает с экземпляром wasm, затрагивая только кадры стека внутри него.

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

Связь с другими предложениями

Исключения

Отклонение обещания, вызывающее исключение, означает, что это предложение зависит от предложения исключений wasm.

Корутины

Предложение Андреаса Россберга по сопрограммам также касается приостановки и возобновления выполнения. Однако, несмотря на некоторое концептуальное совпадение, мы не думаем, что эти предложения конкурируют друг с другом. Оба полезны, потому что они ориентированы на разные варианты использования. В частности, предложение сопрограмм позволяет переключаться между сопрограммами внутри wasm, в то время как предложение await позволяет всему экземпляру ожидать внешней среды. И то, как выполняются обе вещи, приводит к различным характеристикам.

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

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

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

Теоретически эти два предложения могут дополнять друг друга. Возможно, await может быть как-то одной из инструкций в предложении сопрограмм? Другой вариант — позволить await работать с сопрограммой (фактически предоставляя экземпляру wasm простой способ ожидания результатов сопрограммы).

ВАСИ#276

По стечению обстоятельств WASI #276 был опубликован @tqchen как раз в тот момент, когда мы заканчивали писать это. Мы очень рады видеть это, поскольку оно разделяет наше мнение о том, что сопрограммы и поддержка асинхронности — это отдельные функции.

Мы считаем, что инструкция await могла бы помочь реализовать что-то очень похожее на то, что предлагается там (вариант C3), с той разницей, что не нужно было бы специальных асинхронных системных вызовов, а некоторые системные вызовы могли бы возвращать waitref , который затем может быть await -ed.

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

Угловой случай: экземпляр wasm => экземпляр wasm => await

В среде JS, когда экземпляр wasm приостанавливается, он немедленно возвращается тому, кто его вызвал. Мы описали, что происходит, если вызывающий объект из JS, и то же самое происходит, если вызывающий объект является браузером (например, если мы сделали setTimeout на экспорте wasm, который приостанавливается; но там не происходит ничего интересного, так как возвращенное обещание просто игнорируется). Но есть и другой случай, когда вызов исходит от wasm, то есть когда экземпляр wasm A напрямую вызывает экспорт из экземпляра B , а B делает паузу. Пауза заставляет нас немедленно выйти из B и вернуть Promise .

Когда вызывающим кодом является JavaScript, как динамический язык, это менее проблематично, и на самом деле разумно ожидать, что вызывающий код проверит тип, как обсуждалось ранее. Когда вызывающим является WebAssembly, который имеет статический тип, это неудобно. Если мы не сделаем что-то в предложении для этого, тогда значение будет приведено, в нашем примере, из Promise к любому экземпляру, который ожидает A (если i32 , оно будет приведено к 0 ). Вместо этого мы предполагаем, что произошла ошибка:

  • Если экземпляр wasm вызывает (напрямую или с использованием call_indirect ) функцию из другого экземпляра wasm, и во время работы в другом экземпляре выполняется await , то создается исключение RuntimeError брошенный с места await .

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

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

Другим вариантом здесь является распространение паузы на вызывающий wasm, то есть все wasm будут приостанавливаться на всем пути к JS, потенциально охватывая несколько экземпляров wasm. Это имеет некоторые преимущества, например, границы модуля wasm перестают иметь значение, но также и недостатки, такие как распространение менее интуитивно понятное (автор вызывающего экземпляра может не ожидать такого поведения) и то, что добавление JS в середине может изменить поведение (также потенциально неожиданно). Требование, чтобы у пользователей был промежуточный JS, как упоминалось ранее, кажется менее рискованным.

Другой вариант может состоять в том, чтобы некоторые экспорты wasm были помечены как асинхронные, а другие нет, и тогда мы могли бы статически знать, что есть что, и не допускать неправильных вызовов; но см. пример event_loop_iteration из предыдущего примера, который является распространенным случаем, который нельзя решить, пометив экспорт, и есть также косвенные вызовы, поэтому мы не можем избежать проблемы таким образом.

Рассмотрены альтернативные подходы

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

Другой вариант, который мы рассмотрели, — это как-то пометить импорт, чтобы сказать: «Этот импорт должен быть приостановлен, если он возвращает обещание». Мы обдумывали различные варианты их пометки на стороне JS или на стороне wasm, но не нашли ничего подходящего. Например, если мы пометим импорт на стороне JS, тогда модуль wasm не будет знать, приостанавливается ли вызов импорта или нет, до этапа связывания, когда поступает импорт. То есть вызовы импорта и приостановки будут «перемешаны». Кажется, что проще всего просто создать для этого новую инструкцию await , в которой явно указано ожидание. Теоретически такая возможность может быть полезна и за пределами Интернета (см. примечания выше), поэтому наличие инструкции для всех может сделать все более последовательным в целом.

Предыдущие связанные обсуждения

https://github.com/WebAssembly/design/issues/1171
https://github.com/WebAssembly/design/issues/1252
https://github.com/WebAssembly/design/issues/1294
https://github.com/WebAssembly/design/issues/1321

Спасибо за прочтение, отзывы приветствуются!

Самый полезный комментарий

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

Await имеет гораздо более простое наблюдаемое поведение, чем общие сопрограммы или переключение стека, но люди, занимающиеся виртуальными машинами, с которыми я разговаривал, согласны с @rossberg в том, что в конечном итоге работа виртуальной машины, вероятно, будет одинаковой для обоих. И, по крайней мере, некоторые люди, занимающиеся виртуальными машинами, верят, что мы все равно получим сопрограммы или переключение стека, и что мы можем поддерживать варианты использования await, используя это. Это будет означать создание новой сопрограммы/стека при каждом вызове wasm (в отличие от этого предложения), но, по крайней мере, некоторые специалисты по VM считают, что это можно сделать достаточно быстро.

В дополнение к отсутствию интереса со стороны специалистов по VM, у нас есть несколько решительных возражений против этого предложения от @fgmccabe и @RossTate , как обсуждалось выше. Мы не согласны с некоторыми вещами, но я ценю эти точки зрения и время, потраченное на их объяснение.

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

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

Все 96 Комментарий

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

Относительно: «Учитывая вышеизложенное, естественной реализацией является копирование стека при паузе». Как это будет работать для стека выполнения? Я предполагаю, что большинство движков JIT используют собственный стек выполнения C между JS и wasm, поэтому я не уверен, что в этом контексте означают сохранение и восстановление. Означает ли это предложение, что стек выполнения wasm необходимо каким-то образом виртуализировать? IIUC избежать использования стека C, как это, было довольно сложно, когда python пытался сделать что-то подобное: https://github.com/stackless-dev/stackless/wiki.

Я разделяю то же беспокойство, что и @sbc100. Копирование стека по своей сути является довольно сложной операцией, особенно если ваша виртуальная машина еще не имеет реализации GC.

@sbc100

Означает ли это предложение, что стек выполнения wasm необходимо каким-то образом виртуализировать?

Я должен оставить это разработчикам виртуальных машин, поскольку я не эксперт в этом. И я не понимаю связь с Python без стека, но, возможно, я не знаю, что это достаточно хорошо, чтобы понять связь, извините!

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

(Мы не уверены, могут ли эти подходы работать на виртуальных машинах wasm или нет — надеемся услышать от разработчиков, да или нет, и есть ли варианты получше!)

@лахланснефф

Не могли бы вы объяснить более подробно, что вы подразумеваете под упрощением GC? Я не понимаю.

Сборщики мусора @kripken часто (но не всегда) могут проходить по стеку, что необходимо, если вам нужно переписать указатели в стеке, чтобы они указывали на новый стек. Возможно, кто-то, кто знает больше об АО, может подтвердить или опровергнуть это.

@лахланснефф

Спасибо, теперь я понимаю, что вы говорите.

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

Я прошу прощения за терминологию «копировать стек» в предложении — я вижу, что это было недостаточно ясно, основываясь на ваших отзывах и отзывах @sbc100 . Опять же, мы не хотим предлагать конкретный подход к реализации виртуальной машины. Мы просто хотели сказать, что если копирование стека необходимо в каком -то подходе, это не будет проблемой для скорости.

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

Я очень рад видеть это предложение. В Lucet уже некоторое время есть операторы yield и resume , и мы используем их именно для взаимодействия с асинхронным кодом, работающим в хост-среде Rust.

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

Это предложение звучит великолепно! Мы пытались немного разобраться в том, как управлять асинхронным кодом на wasmer-js (поскольку у нас нет доступа к внутренностям виртуальной машины в контексте браузера).

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

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

Кажется, .await можно вызвать в JsPromise внутри функции Rust, используя wasm-bindgen-futures ? Как это может работать без предложенной здесь инструкции await ? Извините за свое невежество, я ищу решения для вызова fetch внутри wasm и изучаю Asyncify, но кажется, что решение Rust проще. Что мне здесь не хватает? Может ли кто-нибудь прояснить это для меня?

Я очень взволнован этим предложением. Основным преимуществом этого предложения является его простота, поскольку мы можем создавать API-интерфейсы, которые синхронизируются с POV wasm, и это значительно упрощает портирование приложений без необходимости явно думать о обратных вызовах и async/await. Это позволило бы нам внедрить машинное обучение на основе WASM и WebGPU в нативную виртуальную машину wasm с помощью единого нативного API и работать как в сети, так и в нативной среде.

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

int test() {
   await();
   return 1;
}

Сигнатура соответствующей функции () => i32 . Согласно новому предложению, вызовы test могут либо возвращать i32, либо Promise<i32> . Обратите внимание, что сложнее попросить пользователя статически объявить новую подпись (из-за стоимости переноса кода и могут быть косвенные вызовы внутри функции, о которых мы не знаем, что ожидаются вызовы).

Должны ли мы иметь отдельный режим вызова в экспортируемой функции (например, асинхронный вызов), чтобы указать, что ожидание разрешено во время выполнения?

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

Если я правильно понимаю это предложение, я думаю, что это примерно эквивалентно снятию ограничения на то, что await в JS можно использовать только в функциях async . То есть на стороне wasm waitref может быть externref и вместо инструкции await у вас может быть импортированная функция $await : [externref] -> [] , а на стороне JS вы можете указать foo(promise) => await promise в качестве импортируемой функции. С другой стороны, если бы вы были JS-кодом, который хотел await на промисе вне функции async , вы могли бы предоставить это промис модулю wasm, который просто вызывает await на входе. Это правильное понимание?

@RossTate Не совсем так, AIUI. Код wasm может await обещание (назовем это promise1 ), но результат будет только при выполнении wasm, а не JS. Код wasm вернет другое обещание (назовем его promise2 ) вызывающей стороне JS. Когда promise1 разрешается, выполнение wasm продолжается. Наконец, когда этот код wasm завершается нормально, тогда promise2 разрешается с результатом функции wasm.

@tqchen

Должны ли мы иметь отдельный режим вызова в экспортируемой функции (например, асинхронный вызов), чтобы указать, что ожидание разрешено во время выполнения?

Интересно - в чем вы видите выгоду? Как вы сказали, на самом деле нет никакого способа убедиться, что экспорт в конечном итоге сделает await или нет в обычных ситуациях переноса, поэтому в лучшем случае его можно использовать только иногда. Может быть, это поможет виртуальным машинам внутри?

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

С точки зрения пользователя это также делает написание кода более последовательным. Например, пользователь мог бы написать следующий код, даже если тест не вызывает ожидание, а системный интерфейс автоматически возвращает Promise.resolve(test()).

await inst.exports_async.test();

Кажется, .await можно вызвать в JsPromise внутри функции Rust, используя wasm-bindgen-futures ? await ? Извините за свое невежество, я ищу решения для вызова fetch внутри wasm и изучаю Asyncify, но кажется, что решение Rust проще. Может ли кто-нибудь прояснить это для меня?

@malbarbo Между ними мало совпадений, несмотря на схожие варианты использования; то, что делает Rust, — это, по сути, полные сопрограммы, которые больше входят в сферу действия другого связанного предложения.

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

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

С точки зрения пользователя это также делает написание кода более последовательным. Например, пользователь мог бы написать следующий код, даже если тест не вызывает ожидание, а системный интерфейс автоматически возвращает Promise.resolve(test()).

@tqchen Обратите внимание, что пользователь уже может сделать это, как показано в примере в тесте предложения. То есть JavaScript уже поддерживает и обрабатывает как синхронные, так и асинхронные значения в операторе await одинаковым образом.

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

А, спасибо за исправление, @binji.

В этом случае следующее примерно эквивалентно? Добавьте функцию WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") в JS API. Предположим, что moduleBytes имеет несколько импортов плюс дополнительный импорт import "name1" "name2" (func (param externref)) . Затем эта функция создает экземпляры импорта со значениями, заданными imports , и создает экземпляр дополнительного импорта с тем, что концептуально await . Когда экспортируемые функции создаются из этого модуля, они становятся защищенными, поэтому при вызове этого await он поднимается по стеку, чтобы найти первую защиту, а затем копирует содержимое стека в новый промис, который затем тут же вернулся.

Будет ли это работать? Я считаю, что это предложение может быть сделано исключительно путем изменения JS API без необходимости изменения самой WebAssembly. Конечно, даже тогда он по-прежнему добавляет много полезного функционала.

@kripken Как будет обрабатываться функция start ? Будет ли он статически запрещать await или как-то взаимодействовать с созданием экземпляра Wasm?

@malbarbo wasm-bindgen-futures позволяет запускать код async на Rust. Это означает, что вы должны написать свою программу асинхронным способом: вы должны пометить свои функции как async , и вам нужно использовать .await . Но это предложение позволяет запускать асинхронный код без использования async или .await , вместо этого он выглядит как обычный синхронный вызов функции.

Другими словами, в настоящее время вы не можете использовать синхронные API-интерфейсы ОС (например, std::fs ), потому что в Интернете есть только асинхронные API-интерфейсы. Но с этим предложением вы можете использовать синхронные API-интерфейсы ОС: внутри они будут использовать промисы, но будут выглядеть синхронно с Rust.

Даже если это предложение будет реализовано, wasm-bindgen-futures по-прежнему будет существовать и по-прежнему будет полезен, потому что он обрабатывает другой вариант использования (запуск функций async ). А функции async полезны, потому что их можно легко распараллелить.

@RossTate Кажется, ваше предложение очень похоже на предложение, описанное в «Рассмотренных альтернативных подходах»:

Другой вариант, который мы рассмотрели, — это как-то пометить импорт, чтобы сказать: «Этот импорт должен быть приостановлен, если он возвращает обещание». Мы обдумывали различные варианты их пометки на стороне JS или на стороне wasm, но не нашли ничего подходящего. Например, если мы пометим импорт на стороне JS, тогда модуль wasm не будет знать, приостанавливается ли вызов импорта или нет, до этапа связывания, когда поступает импорт. То есть вызовы импорта и приостановки будут «перемешаны». Кажется, что самое простое — просто создать новую инструкцию для этого, await, в которой явно указано ожидание. Теоретически такая возможность может быть полезна и за пределами Интернета (см. примечания выше), поэтому наличие инструкции для всех может сделать все более последовательным в целом.

Как будет обрабатываться функция запуска? Будет ли он статически запрещать ожидание или каким-то образом взаимодействовать с созданием экземпляра Wasm?

@Pauan Мы не обсуждали это специально, но я думаю, что ничто не мешает нам также разрешить await в start . В этом случае обещание, возвращенное из instantiate{Streaming} , по-прежнему будет естественным образом разрешаться/отклоняться, когда стартовая функция полностью завершит выполнение, с той лишь разницей, что она будет ждать промисов await .

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

@RReverser Как это будет работать для синхронного new WebAssembly.Instance (который используется в рабочих процессах)?

Интересный момент @Pauan о начале!

Да, для синхронного создания экземпляров это кажется рискованным - если await разрешено, странно, если кто-то вызывает экспорт, пока он приостановлен. Запрет await там может быть самым простым и безопасным. (Возможно, также при асинхронном запуске для согласованности, кажется, нет важных вариантов использования, которые могли бы предотвратить? Нужно больше подумать.)

(который используется в рабочих)?

Хм хорошая мысль; Я не думаю, что его нужно использовать в Workers, но, поскольку этот API уже существует, возможно, он может вернуть обещание? Я видел это как полупопулярный новый шаблон для возврата thenables из конструктора различных библиотек, хотя не уверен, что это хорошая идея делать это в стандартном API.

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

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

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

Как насчет отмены? Это не реализовано в обещаниях JS, и это вызывает некоторые проблемы.

@Кангз

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

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

Текущий текст, возможно, недостаточно ясен в этом отношении. По первому абзацу да, это разрешено, см. раздел «Пояснения»: It is ok to call into the WebAssembly instance while a pause has occurred, and multiple pause/resume events can be in flight at once.

Для второго абзаца нет — вы не можете получать события раньше, и вы не можете заставить JS разрешить промис раньше, чем это было бы. Попробую подытожить по-другому:

  • Когда wasm останавливается на промисе A, он возвращается к тому, кто его вызвал, и возвращает новый промис B.
  • Wasm возобновляется, когда разрешается Promise A. Это происходит в обычное время , что означает, что в цикле событий JS все нормально.
  • После возобновления работы wasm и его завершения только тогда разрешается Promise B.

Так что, в частности, Promise B должен разрешаться после Promise A. Вы не можете получить результат Promise A раньше, чем JS сможет его получить.

Другими словами: поведение этого предложения может быть полифиллировано с помощью Asyncify + некоторого JS, который использует промисы вокруг него.

@RReverser , я не думаю, что это одно и то же, но сначала, я думаю, нам нужно кое-что прояснить (если это еще не было прояснено, в этом случае мне жаль, что я это пропустил).

Может быть несколько вызовов из JS в один и тот же экземпляр wasm в одном и том же стеке в одно и то же время. Если await выполняется экземпляром, какой вызов приостанавливается и возвращает обещание?

Для второго абзаца нет — вы не можете получать события раньше, и вы не можете заставить JS разрешить промис раньше, чем это было бы.

Извините, я думаю, что мой вопрос не был ясен. В настоящее время приложения «основного цикла» на C++ используют emscripten_set_main_loop , так что между каждым запуском функции фрейма управление возвращается браузеру, и можно обрабатывать ввод или другие события.

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

int main() {
  while (true) {
    frame();
    processEvents();
  }
}

// polyfillable with ASYNCIFY!
void processEvents() {
  __builtin_await(EM_ASM(
    new Promise((resolve, reject) => {
      setTimeout(0, () => resolve());
    })
  ))
}

@Kangz Это должно работать, да (за исключением того, что у вас есть небольшая проблема с порядком аргументов в вашем коде setTimeout, плюс его можно упростить):

int main() {
  while (true) {
    frame();
    processEvents();
  }
}

// polyfillable with ASYNCIFY!
void processEvents() {
  __builtin_await(EM_ASM_WAITREF(
    return new Promise(resolve => setTimeout(resolve));
  ));
}

Может быть несколько вызовов из JS в один и тот же экземпляр wasm в одном и том же стеке в одно и то же время. Если ожидание выполняется экземпляром, какой вызов приостанавливается и возвращает обещание?

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

@Kangz Извините, я неправильно вас понял. Да, как сказал @RReverser , это должно работать, и это хороший пример предполагаемого варианта использования!

Как вы сказали, он может быть заполнен с помощью Asyncify, и фактически он эквивалентен тому же коду с Asyncify сегодня, заменив __builtin_await вызовом emscripten_sleep(0) (который делает setTimeout(0) ) .

Спасибо, @RReverser , за разъяснение. Я думаю, было бы полезно перефразировать описание, чтобы сказать, что (самый последний) вызов экземпляра приостанавливается, а не сам экземпляр.

В таком случае это звучит почти эквивалентно добавлению в JS следующих двух примитивных функций: promise-on-await(f) и await-for-promise(p) . Первый вызывает f() , но если во время выполнения f() делается вызов await-for-promise(p) , вместо этого возвращается новый промис, который возобновляет выполнение после того, как p разрешится. и сам разрешается после завершения этого выполнения (или снова вызывает await-for-promise ). Если вызов await-for-promise выполняется в контексте нескольких promise-on-await s, то самый последний из них возвращает Promise. Если вызов await-for-promise выполняется за пределами любого promise-on-await , происходит что-то плохое (точно так же, как если код экземпляра start выполняет await ).

Имеет ли это смысл?

@RossTate Это довольно близко, да, и отражает общую идею. (Но, как вы сказали, только почти эквивалентно, так как его нельзя использовать для полифилла, и в нем отсутствует конкретная обработка границ wasm/JS.)

Спасибо за предложение перефразировать этот текст. Я держу список таких заметок из обсуждения здесь. (Я не уверен, стоит ли применять их к первому посту, так как кажется менее запутанным не менять его со временем?)

@RossTate Интересно... Мне это нравится! Это делает асинхронный характер вызова явным ( promise-on-await требуется для любого потенциально асинхронного вызова) и не требует каких-либо изменений в Wasm. Это также имеет (некоторый) смысл, если вы удалите Wasm из середины - если promise-on-await вызывает await-for-promise напрямую, то он возвращает Promise .

@kripken , не могли бы вы рассказать подробнее, почему это будет иначе? Я не совсем понимаю, почему здесь важна граница Wasm/JS.

@binji Я просто имел в виду, что такие функции в JS не позволят wasm сделать что-то подобное. Называть их импортом из wasm не получится. Нам все еще нужен способ заставить wasm выйти на границу и т. д. возобновляемым способом, не так ли?

@kripken верно, я думаю, что в этот момент импорт await-for-promise должен был бы функционировать как встроенный в Wasm.

Я думал, что вместо добавления инструкции await в wasm такой модуль вместо этого импортирует await-for-promise и вызывает это. Точно так же вместо изменения экспортированных функций код JS будет вызывать их внутри promise-on-await . Это означает, что примитивы JS будут обрабатывать всю работу со стеком, включая стек WebAssembly. Это также было бы более гибким, например, если вы хотите, чтобы вы могли дать модулю обратный вызов JS, который затем может вернуться в модуль и приостановить внешний вызов вместо внутреннего предложения — все зависит от того, выбирает ли код JS обернуть вызов в promise-on-await или нет. Я не думаю, что вам нужно что-то менять в самом wasm.

Мне было бы интересно услышать, что @syg думает об этих потенциальных примитивах JS.

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

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

myList.forEach((item) => {
  .. call something which ends up pausing ..
});

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

for (let i of something) {
  .. call something which ends up pausing ..
}

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

Но кроме того, это позволяет избежать добавления await и waitref в базовую спецификацию wasm, но это крошечные дополнения, поскольку они ничего не делают в базовой спецификации. Текущее предложение уже имеет 99% сложности на стороне JS. И ваше предложение IIUC обменяет это небольшое дополнение к спецификации wasm на гораздо более крупные дополнения на стороне JS, что делает веб-платформу в целом более сложной, и это излишне, поскольку все это предназначено для wasm. Кроме того, определение await в основной спецификации wasm дает преимущество, так как оно может быть полезно за пределами Интернета.

Может быть, я что-то упустил в вашем предложении, извините, если это так. В целом, мне любопытно, какова ваша мотивация для попытки избежать дополнения к основной спецификации wasm?

Я не думаю, что эти примитивы имеют большое значение для js, и я думаю, что от этого выиграют больше реализаций wasm, чем в браузерах. Мне все еще любопытно, почему возобновляемые исключения (примерно эффекты) не подходят для этого варианта использования.

Мой комментарий был комбинацией обоих. На высоком уровне я пытаюсь выяснить, есть ли способ перефразировать предложение как чистое обогащение JS API (и аналогично тому, как другие хосты будут взаимодействовать с модулями wasm). Упражнение помогает оценить, действительно ли wasm нуждается в изменении, и помогает определить, действительно ли предложение тайно добавляет новые примитивы в JS, которые разработчики JS могут одобрить или не одобрить. То есть, если нельзя обойтись просто импортированным await : func (param externref) (result externref) , то вполне вероятно, что это добавление нового функционала в JS.

Что касается простоты изменений в wasm, есть еще много вещей, которые нужно учитывать, например, что делать с вызовами модуля к модулю, что делать, когда экспортированные функции возвращают значения GC, содержащие указатели на функции, которые могут выполнять await после завершения вызова и т.д.

Возвращаясь к упражнению, как вы указали, есть веские причины для захвата только стека wasm. Это возвращает меня к моему более раннему предложению, хотя и несколько измененному с некоторой новой точки зрения. Добавьте функцию WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") в JS API. Предположим, что moduleBytes имеет несколько импортов плюс дополнительный импорт import "name1" "name2" (func (param externref) (result externref)) . Затем instantiateAsync создает экземпляр других импортов moduleBytes просто со значениями, заданными imports , и создает экземпляр дополнительного импорта с тем, что концептуально await-for-promise . Когда экспортируемые функции создаются из этого экземпляра, они защищаются (концептуально с помощью promise-on-await ), так что при вызове этого await-for-promise он проходит вверх по стеку, чтобы найти первую защиту, а затем копирует содержимое стек в новый Promise, который затем немедленно возвращается. Теперь у нас есть те же самые примитивы, о которых я упоминал выше, но они больше не являются первоклассными, и этот ограниченный шаблон гарантирует, что будет захвачен только стек wasm. При этом WebAssembly не нужно менять для поддержки шаблона.

Мысли?

@devsnek

Мне все еще любопытно, почему возобновляемые исключения (примерно эффекты) не подходят для этого варианта использования.

Они вариант в этом пространстве, конечно.

Насколько я понимаю из последней презентации @rossberg , он изначально хотел пойти по этому пути, но затем изменил направление, чтобы применить подход сопрограммы. См. слайд с заголовком «Проблемы». После этого слайда описываются сопрограммы, которые являются еще одним вариантом в этом пространстве. Так что, может быть, ваш вопрос больше к @rossberg , который, возможно, может прояснить?

Это предложение направлено на решение проблемы синхронизации/асинхронности, которая не требует такой мощности, как возобновляемые исключения или сопрограммы. Они сосредоточены на внутренних взаимодействиях внутри модуля wasm, в то время как мы сосредоточены на взаимодействии между модулем wasm и внешней средой (потому что именно здесь возникает проблема синхронизации/асинхронности). Вот почему нам просто нужна одна новая инструкция в основной спецификации wasm, и почти вся логика в этом предложении находится в спецификации wasm JS. И это означает, что вы можете ждать Promise следующим образом:

call $get_promise
await
;; use it!

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

@РоссТейт

То есть, если нельзя обойтись просто импортированным await:func(param externref)(result externref), то вполне вероятно, что это добавляет новый функционал в JS.

Я не следую этому выводу, извините. Но мне кажется окольным путем. Если вы считаете, что это предложение добавляет новые функции в JS, почему бы не показать это напрямую? (Я твердо верю, что это не так, но мне любопытно, если вы обнаружите, что мы допустили ошибку!)

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

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

Я считаю, что основные дополнения к спецификации wasm будут состоять в основном из списка await , скажем, что он предназначен для «ожидания чего-то», и все. Вот почему я написал That's it for the core wasm spec! в предложении. Если я ошибаюсь, покажите мне в основной спецификации wasm, где нам нужно добавить больше.

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

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

Разве эта идея функционально не совпадает со вторым абзацем в предложении Alternative approaches considered ? Такое можно сделать, но мы объяснили, почему мы думаем, что это менее хорошо.

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

Я думаю, что предложение @RossTate действительно очень похоже на то, что упоминается в разделе «Рассмотренные альтернативные подходы». Поэтому я думаю, что нам следует более подробно обсудить, почему этот подход был отклонен. Я думаю, мы все можем согласиться с тем, что решение, которое не требует изменений спецификации wasm, было бы предпочтительнее, если мы сможем сделать JS-сторону работоспособной. Я пытаюсь понять недостатки, которые вы изложили в этом разделе, и почему они делают решение, основанное только на JS, таким неприемлемым.

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

Нет! См. обсуждаемые варианты использования вне Интернета здесь. Без await в спецификации wasm мы бы закончили тем, что каждая платформа делала бы что-то специальное: среда JS импортировала бы что-то, другие места создавали бы новые API-интерфейсы с пометкой «синхронный» и т. д. Экосистема wasm быть менее последовательным, было бы сложнее переместить васм из Интернета в другие места и т. д.

Но да, мы должны максимально упростить основную часть спецификации wasm. Я думаю, это делает это? 99% логики находится на стороне JS (но @RossTate , кажется, не согласен, и мы все еще пытаемся это выяснить - я задал конкретные вопросы в своем последнем ответе, который, я надеюсь, продвинет ситуацию).

Я считаю, что основные дополнения к спецификации wasm будут состоять в основном из списка await , скажем, что он предназначен для «ожидания чего-то», и все.

Если эту семантику нельзя формализовать более точно, это привнесет в спецификацию двусмысленность или поведение, определяемое реализацией. До сих пор мы избегали этого (при значительных затратах в случае с SIMD), так что это определенно то, что я хотел бы видеть закрепленным. Я не думаю, что само предложение должно измениться, чтобы сделать его более формальным, но «ждите чего-то» следует перефразировать в точной терминологии, уже используемой в спецификации.

Говорит ли спецификация ядра wasm что-нибудь о вызовах между модулями?

Импорт экземпляра может быть создан с экспортом другого экземпляра. Насколько я понимаю JS API (и принцип композиционности wasm), вызов такого импорта концептуально является прямым вызовом любой функции, экспортируемой другим экземпляром. То же самое касается (косвенных) вызовов функциональных значений, таких как funcref , которые передаются между двумя экземплярами.

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

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

Обратите внимание, что это подразумевает, что если бы f было значением экспортируемой унарной функции некоторого экземпляра wasm, то объект параметров инстанцирования {"some" : {"import" : f}} семантически отличался бы от {"some" : {"import" : (x) => f(x)}} , потому что вызовы к первому останется в стеке wasm, тогда как вызовы ко второму войдут в стек JS, хотя и едва ли. До сих пор эти объекты параметров инстанцирования считались эквивалентными. Я могу объяснить, почему это полезно с точки зрения миграции кода/языкового взаимодействия, но на данный момент это было бы отступлением.

Разве эта идея функционально не аналогична второму абзацу в разделе «Альтернативные подходы», рассматриваемому в предложении? Такое можно сделать, но мы объяснили, почему мы думаем, что это менее хорошо.

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

Тот факт, что это предложение настолько легкое со стороны wasm, объясняется тем, что инструкция await кажется семантически идентичной вызову импортированной функции. Конечно, условности имеют значение, как вы указываете! Но await — не единственная функциональность, для которой это справедливо; то же самое верно для большинства импортированных функций. В случае с await я считаю, что проблема соглашения может быть решена за счет того, что модули с этой функциональностью имеют предложение import "control" "await" (func (param externref) (result externref)) , а среды, поддерживающие эту функциональность, всегда создают экземпляр этого импорта. с соответствующим обратным вызовом.

Кажется, это дает решение, которое экономит массу работы, не меняя wasm, но при этом обеспечивая кросс-платформенную переносимость, которую вы ищете. Но я все еще работаю над тем, чтобы понять нюансы предложения, и я уже многое пропустил!

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

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

Например, следуя вашему аргументу, что-то вроде memory.grow или atomic.wait также может быть сделано как import "control" "memory_grow" или import "control" "atomic_wait" соответственно, но они не такие, как они. не обеспечивают тот же уровень возможностей взаимодействия и статического анализа (как на стороне виртуальной машины, так и на стороне инструментов), что и настоящая инструкция.

Вы можете возразить, что memory.grow как инструкция по-прежнему полезна для случаев, когда память не экспортируется, но atomic.wait определенно может быть реализована вне ядра. На самом деле, это очень похоже на await , за исключением уровня, на котором происходит пауза/возобновление, и того факта, что await как функция потребует гораздо больше магии, чем atomic.wait поскольку он должен иметь возможность взаимодействовать со стеком виртуальной машины, а не просто блокировать текущий поток до тех пор, пока значение не изменится.

@tlively

«ждите чего-то» следует перефразировать в точной терминологии, уже используемой в спецификации.

Определенно да. Теперь я могу предложить более конкретный текст, если это будет полезно:

When an await instruction is executed on a waitref, the host environment is requested to do some work. Typically there would be a natural meaning to what that work is based on what a waitref is on a specific host (in particular, waiting for some form of host event), but from the wasm module's point of view, the semantics of an await are similar to a call to an imported host function, that is: we don't know exactly what the host will do, but at least expect to give it certain types and receive certain results; after the instruction executes, global state (the store) may change; and an exception may be thrown.

The behavior of an await from the host's perspective may be very different, however, from a call to an imported host function, and might involve something like pausing and resuming the wasm module. It is for this reason that this instruction is defined. For the instruction to be usable on a particlar host, the host would need to define the proper behavior.

Кстати, еще одно сравнение, которое пришло мне в голову, когда я писал это, — это подсказки выравнивания при загрузке и сохранении. Wasm поддерживает невыровненные загрузки и сохранения, поэтому подсказки не могут привести к другому поведению, наблюдаемому модулем wasm (даже если подсказка неверна), но для хоста они предлагают совсем другую реализацию на определенных платформах (которая может быть более эффективной). Так что это пример разных инструкций без внутренне наблюдаемой разной семантики, как сказано в спецификации: The alignment in load and store instructions does not affect the semantics .

@РоссТейт

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

Звучит хорошо, и приятно знать, спасибо, я пропустил эту часть.

Думаю, это объясняет мне часть нашего непонимания. Вызовы модуля => модуля не входят в спецификацию wasm atm, о чем я говорил ранее. Но похоже, что вы думаете о будущей спецификации, где они могут быть. В любом случае, здесь это не выглядит проблемой, поскольку композиционность точно определяет, как должен вести себя ожидание в этой ситуации (это не то, что я предлагал ранее!, но имеет больше смысла).

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

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

Если await ниже межмодульных вызовов определены для перехвата, тогда потребуется указать обход вверх по стеку вызовов, чтобы проверить, существует ли межмодульный вызов до последнего фиктивного кадра, созданного вызовом с хоста. (§ 4.5.5). Это было бы досадным осложнением в спецификации. Но я согласен с Россом в том, что наличие ловушки межмодульных вызовов было бы нарушением композиционности, поэтому я бы предпочел семантику, при которой весь стек замораживается до последнего вызова с хоста. Самый простой способ указать это — сделать await похожим на вызов функции хоста (§ 4.4.7.3), как вы говорите, @kripken. Но вызовы хост-функций полностью недетерминированы, поэтому лучшим именем для инструкции с точки зрения базовой спецификации может быть undefined . И в этот момент я на самом деле начинаю предпочитать встроенный импорт, который всегда будет предоставляться веб-платформой (и WASI для переносимости), потому что основная спецификация сама по себе не выигрывает от наличия инструкции undefined IMO.

Семантически вызов к хост-среде, который возвращает waitref плюс await , является просто блокирующим вызовом, верно?

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

@RReverser , я понимаю, что вы говорите о внутренних свойствах. При принятии решения о том, когда операцию следует определять с помощью неинтерпретируемых функций, а не инструкций, необходимо принять решение. Я думаю, что одним из факторов в этом решении является рассмотрение того, как оно взаимодействует с другими инструкциями. memory.grow влияет на поведение других инструкций памяти. У меня не было возможности ознакомиться с предложением Threads, но я полагаю, что atomic.wait влияет или зависит от поведения других инструкций синхронизации. Затем необходимо обновить спецификацию, чтобы формализовать эти взаимодействия.

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

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

@kripken :

Насколько я понимаю из последней презентации @rossberg , он изначально хотел пойти по этому пути, но затем изменил направление, чтобы применить подход сопрограммы. См. слайд с заголовком «Проблемы». После этого слайда описываются сопрограммы, которые являются еще одним вариантом в этом пространстве. Так что, может быть, ваш вопрос больше к @rossberg , который, возможно, может прояснить?

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

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

Что касается нынешнего предложения, я все еще пытаюсь его обдумать. :)

В частности, кажется, что await можно использовать в любой старой функции Wasm, я правильно понимаю? Если да, то это сильно отличается от JS, который позволяет await только в асинхронных функциях. И это очень важное ограничение, потому что оно позволяет движкам компилировать await путем _локального_ преобразования одной (асинхронной) функции!

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

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

@россберг

В частности, кажется, что await можно использовать в любой старой функции Wasm, я правильно понимаю? Если это так, то это сильно отличается от JS, который позволяет ожидать только в асинхронных функциях.

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

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

Однозначно глобальное преобразование программы здесь не предполагается! Извините, если это было непонятно.

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

  • Только весь экземпляр wasm может приостанавливаться. Это не для переключения стека внутри модуля. (В частности, именно поэтому это предложение может не иметь дополнений к основной спецификации wasm и быть полностью на стороне wasm JS; до сих пор некоторые люди предпочитают это, и я думаю, что любой вариант может работать.)
  • Корутины явно объявляют стеки, а await — нет.
  • стеки ожидания могут быть возобновлены только один раз, разветвления/возврата более одного раза не происходит (не уверен, будет ли это в вашем предложении или нет?).
  • Модель производительности здесь совсем другая. await будет ждать промиса в JS, который уже имеет минимальные накладные расходы и задержку. Так что это нормально, если реализация имеет некоторые накладные расходы, когда мы на самом деле делаем паузу, и нас это волнует меньше, чем, вероятно, сопрограммы.

Учитывая эти факторы и то, что наблюдаемое поведение этого предложения заключается в том, что весь экземпляр wasm приостанавливается, могут быть различные способы его реализации. Например, вне Интернета на виртуальной машине, на которой запущен один экземпляр wasm, он может просто запустить свой цикл событий, пока не придет время возобновить работу wasm. В Интернете одним из подходов к реализации может быть: когда происходит ожидание, копировать весь стек wasm из текущей позиции в то место, где мы вызвали wasm; сохранить это на стороне; чтобы возобновить, скопируйте его обратно и просто продолжайте оттуда. Также могут быть другие подходы или вариации (некоторые, возможно, без копирования, но опять же, избегание накладных расходов на копирование здесь на самом деле не имеет решающего значения!).

Извините за длинный пост и некоторые повторения самого текста предложения, но я надеюсь, что это поможет прояснить некоторые моменты, на которые вы ссылались?

Я думаю, что здесь есть что обсудить с точки зрения реализации. Пока комментарий @acfoltzer о Люсет обнадеживает!

Просто чтобы прояснить некоторые фразы в последнем комментарии @kripken , приостанавливается не весь экземпляр wasm. Приостанавливается только самый последний вызов из хост-фрейма в wasm в стеке, а затем хост-фрейму вместо этого возвращается соответствующий промис (или соответствующий аналог для хоста). См. здесь соответствующие более ранние разъяснения.

Хм, я не вижу, как это имеет значение. Когда вы ждете где-то глубоко внутри Wasm, вам нужно будет захватить весь стек вызовов, по крайней мере, от входа хоста до этой точки. И вы можете поддерживать эту приостановку (т. е. этот сегмент стека) в активном состоянии столько, сколько хотите, одновременно выполняя другие вызовы сверху или создавая дополнительные приостановки. И вы можете возобновить откуда-то еще (я думаю?). Разве для этого не требуется весь механизм реализации разделенных продолжений? Только то, что подсказка устанавливается при входе в Wasm, а не отдельной конструкцией.

@россберг

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

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

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

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

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

Правильно, как atomic.wait .

@таралкс

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

С одной стороны, повторный вход может быть полезен, например, игровой движок может загрузить файл и не захотеть, чтобы пользовательский интерфейс при этом полностью приостанавливался (сегодня Asyncify позволяет это делать). Но, с другой стороны, возможно, повторный вход может быть запрещен, но приложение может создать для этого несколько экземпляров одного и того же модуля (все импортируют одну и ту же память, изменяемые глобальные переменные и т. д.?), поэтому повторный вход будет вызовом к другому экземпляру. Я думаю, мы можем заставить это работать в цепочках инструментов (было бы эффективное ограничение на количество повторных входов, активных одновременно — равное количеству экземпляров — что кажется нормальным).

Так что, если ваше упрощение поможет виртуальным машинам, его определенно стоит рассмотреть!

(Обратите внимание, что, как обсуждалось ранее, я не думаю, что нам нужна полная трансформация программы здесь с любым из обсуждаемых вариантов. Вам это нужно только в том случае, если вы находитесь в плохой ситуации, в которой находится Asyncify, когда это все, что вы можете сделать в уровень цепочки инструментов. Что касается ожидания, в худшем случае, как обсуждалось с @rossberg , вы можете делать то, что предложение сопрограмм делало бы внутри. Но ваша идея потенциально очень интересна, если она делает вещи проще!)

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

Я не уверен, что это звуковая функция. Мне кажется, что это приведет к _неожиданному параллелизму_ в приложении. Нативное приложение, которое загружает активы во время рендеринга, будет использовать 2 внутренних потока, и каждый поток будет отображаться в WebWorker + SharedArrayBuffer. Если приложение использует потоки, оно также может использовать синхронные веб-примитивы от WebWorkers (поскольку они разрешены, по крайней мере, в некоторых случаях). В противном случае всегда можно сопоставить асинхронные операции в основном потоке с блокирующими операциями в воркере с помощью Atomics.wait (например).

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

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

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

@alexp-sssup

Мне кажется, что это приведет к неожиданному параллелизму в приложении.

Однозначно, да - это нужно делать очень осторожно, и можно что-то сломать. У нас есть смешанный опыт использования Asyncify, хороший и плохой (например, допустимый вариант использования: файл загружается в JS, и JS вызывает wasm, чтобы malloc некоторое пространство, в которое его можно скопировать, перед возобновлением). Но в любом случае повторный вход в любом случае не является важной частью этого предложения.

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

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

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

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

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

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

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

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

Текущее решение, которое переписывает модуль Wasm, не страдает от этих проблем, но вместо этого требует значительных затрат на размер файла.

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

худшая производительность

У вас есть какой-то эталон?

Да, я могу поделиться им, когда вернусь на работу во вторник (или, что более вероятно, в среду), или довольно легко сделать один, который просто вызывает пустую асинхронную JS-функцию самостоятельно.

Спасибо. Я мог бы создать микробенчмарк, но это было бы не слишком поучительно.

Ах да, мой тоже микробенчмарк, так как нас интересовало исключительно сравнение накладных расходов.

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

Я думаю, что акцент на скорости атомарного подхода может отвлекать. Как упоминалось ранее, atomics не работает и не будет работать везде (из-за COOP/COEP), а также только worker может использовать подход atomics, так как основной поток не может блокироваться. Это хорошая идея, но для универсального решения нам нужно что-то вроде Await.

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

@taralx О, хорошо, теперь понятно, спасибо.

@таралкс :

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

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

Общий принцип проектирования заключается в том, что операционное поведение никогда не должно зависеть от границ модуля (кроме простой области видимости). Модули — это просто механизм группировки и области видимости в Wasm, и вы хотите сохранить возможность перегруппировки (связывать/объединять/разделять модули) без изменения поведения программы.

@rossberg : это можно обобщить как блокировку доступа к любому модулю Wasm, как предлагалось ранее. Но тогда это, вероятно, слишком ограничивает.

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

Это была моя точка зрения с аргументом полифилла - atomic.wait не нарушает модульность, так что это тоже не должно.

@taralx atomic.wait ссылается на определенное место в определенной памяти. Какую память и расположение будет использовать блокировка await , и как можно контролировать, какие модули совместно используют эту память?

@rossberg , можете ли вы рассказать о сценарии, который, по вашему мнению, нарушается? Я подозреваю, что у нас разные представления о том, как будет работать нереентерабельная версия.

@taralx , рассмотрите возможность загрузки двух модулей A и B, каждый из которых предоставляет некоторую функцию экспорта, скажем, A.f и B.g . Оба могут выполнять await при вызове. Каждой из двух частей клиентского кода передается одна из этих функций, соответственно, и они вызывают их независимо. Они не мешают и не блокируют друг друга. Затем кто-то объединяет или реорганизует A и B в C, ничего не меняя в коде. Внезапно обе части клиентского кода могут неожиданно начать блокировать друг друга. Жуткое действие на расстоянии через скрытое общее состояние.

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

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

(отредактировано)

Хм, да. Итак, импортированная функция может повторно войти в модуль. Мне явно нужно хорошенько подумать об этом.

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

(отредактировал мой комментарий выше)

Спасибо всем за обсуждение!

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

Я предложу пункт повестки дня для следующего заседания ГК через 2 недели, чтобы обсудить это предложение и рассмотреть его для этапа 1, что будет означать открытие репо и более подробное обсуждение открытых вопросов в отдельных вопросах там. (Я считаю, что это правильный процесс, но, пожалуйста, поправьте меня, если я ошибаюсь.)

Просто для информации
Мы будем составлять предложение по коммутации полного стека в аналогичном
временные рамки. Я чувствую, что это может сделать ваш вариант особого случая спорным -
что ты думаешь?
Фрэнсис

В четверг, 28 мая 2020 г., в 15:51 Алон Закаи, [email protected] написал:

Спасибо всем за обсуждение!

Подводя итог, можно сказать, что здесь есть общий интерес, но есть
большие открытые вопросы, например, должно ли это быть 100% на стороне JS или просто
99% - звучит так, как будто первое устранило бы основные проблемы некоторых людей
есть, и это было бы хорошо для веб-кейса, так что это, вероятно, нормально.
Другой большой открытый вопрос заключается в том, насколько это возможно сделать на виртуальных машинах, которые
нам нужно больше информации о.

Я предложу пункт повестки дня для следующего собрания CG через 2 недели для обсуждения
это предложение и рассмотреть его для этапа 1, который будет означать открытие репо
и обсуждение открытых вопросов в отдельных выпусках более подробно там.
(Я считаю, что это правильный процесс, но, пожалуйста, поправьте меня, если я ошибаюсь.)


Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/WebAssembly/design/issues/1345#issuecomment-635649331 ,
или отписаться
https://github.com/notifications/unsubscribe-auth/AAQAXUCLZ4CJVQYEUBK23BLRT3TFLANCNFSM4NEJW2PQ
.

>

Фрэнсис МакКейб
Швеция

@fgmccabe

Мы должны обсудить это обязательно.

В целом, если ваше предложение не сосредоточено на стороне JS, я предполагаю, что это не сделает это спорным (что составляет 99%-100% на стороне JS).

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

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

В случае веб-программ, теперь с WebAssembly эти разные компоненты могут даже быть написаны на разных языках: JS или wasm. На самом деле, многие компоненты могут быть написаны на любом языке; Я буду называть их «амбивалентными» компонентами. Прямо сейчас большинство амбивалентных компонентов написано на JS, но я полагаю, мы все надеемся, что все больше и больше их будет переписано на wasm. Чтобы облегчить эту «миграцию кода», мы должны постараться гарантировать, что переписывание компонента таким образом не изменит его взаимодействия со средой. В качестве игрушечного примера: независимо от того, написан ли конкретный программный компонент «применить» (f, x) => f(x) на JS или на wasm, это не должно влиять на поведение всей программы. Это принцип миграции кода.

К сожалению, все варианты этого предложения нарушают либо программу компоновки модулей, либо принцип миграции кода. Первый нарушается, когда await захватывает стек до того места, где текущий модуль wasm был введен последним, потому что эта граница изменяется по мере того, как модули разделяются или объединяются вместе. Последнее нарушается, когда await захватывает стек до места последнего ввода wasm, потому что эта граница меняется по мере переноса кода из JS в wasm (так что миграция чего-то такого простого, как (f, x) => f(x) из JS to wasm может существенно изменить поведение всей программы).

Я не думаю, что эти нарушения связаны с плохим выбором дизайна этого предложения. Скорее, проблема заключается в том, что это предложение пытается избежать косвенного усиления JS, и эта цель вынуждает его устанавливать искусственные границы, которые нарушают эти принципы. Я полностью понимаю эту цель, но я подозреваю, что эта проблема будет возникать все чаще и чаще: добавление функциональности в WebAssembly с соблюдением этих принципов часто требует косвенного добавления функциональности в JS из-за того, что JS является языком встраивания. Я бы предпочел решить эту проблему в лоб (я действительно понятия не имею, как ее решить). Если нет, то моим вторым предпочтением было бы внести это изменение исключительно в JS API, потому что именно JS является здесь ограничивающим фактором, а не добавлять в WebAssembly инструкции, для которых у wasm нет интерпретации.

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

Это важно, но это не главная причина дизайна здесь.

Основная причина такого дизайна заключается в том, что, хотя я полностью согласен с тем, что принцип композиции имеет смысл для wasm , фундаментальная проблема, с которой мы сталкиваемся в Интернете, заключается в том, что на практике JS и wasm не эквивалентны. У нас есть написанный от руки JS, который является асинхронным, и портированный wasm, который является синхронным. Другими словами, граница между ними и есть та самая проблема, которую мы пытаемся решить. В целом я не уверен, что согласен с принципом композиции, который следует применять к wasm и JS (но, возможно, это должно быть интересным спором).

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

Await имеет гораздо более простое наблюдаемое поведение, чем общие сопрограммы или переключение стека, но люди, занимающиеся виртуальными машинами, с которыми я разговаривал, согласны с @rossberg в том, что в конечном итоге работа виртуальной машины, вероятно, будет одинаковой для обоих. И, по крайней мере, некоторые люди, занимающиеся виртуальными машинами, верят, что мы все равно получим сопрограммы или переключение стека, и что мы можем поддерживать варианты использования await, используя это. Это будет означать создание новой сопрограммы/стека при каждом вызове wasm (в отличие от этого предложения), но, по крайней мере, некоторые специалисты по VM считают, что это можно сделать достаточно быстро.

В дополнение к отсутствию интереса со стороны специалистов по VM, у нас есть несколько решительных возражений против этого предложения от @fgmccabe и @RossTate , как обсуждалось выше. Мы не согласны с некоторыми вещами, но я ценю эти точки зрения и время, потраченное на их объяснение.

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

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

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

aaabbbcccddd00001111 picture aaabbbcccddd00001111  ·  3Комментарии

JimmyVV picture JimmyVV  ·  4Комментарии

beriberikix picture beriberikix  ·  7Комментарии

frehberg picture frehberg  ·  6Комментарии

thysultan picture thysultan  ·  4Комментарии