Сначала я попробовал общее описание проблемы, затем еще несколько деталей, почему обычные подходы не работают. Если вы хотите прочитать эти абстрактные объяснения, продолжайте. В конце я объясню большую проблему и конкретное приложение, поэтому, если вы предпочитаете читать это, перейдите в "Фактическое приложение".
Я использую дочерний процесс node.js для выполнения некоторой вычислительно-интенсивной работы. Родительский процесс работает, но в какой-то момент выполнения он достигает точки, в которой он должен иметь информацию из дочернего процесса, прежде чем продолжить. Поэтому я ищу способ подождать завершения дочернего процесса.
Моя текущая настройка выглядит примерно так:
importantDataCalculator = fork("./runtime");
importantDataCalculator.on("message", function (msg) {
if (msg.type === "result") {
importantData = msg.data;
} else if (msg.type === "error") {
importantData = null;
} else {
throw new Error("Unknown message from dataGenerator!");
}
});
и где-то еще
function getImportantData() {
while (importantData === undefined) {
// wait for the importantDataGenerator to finish
}
if (importantData === null) {
throw new Error("Data could not be generated.");
} else {
// we should have a proper data now
return importantData;
}
}
Итак, когда родительский процесс запускается, он выполняет первый бит кода, создавая дочерний процесс для вычисления данных и продолжает выполнять свою собственную работу. Когда придет время, для этого ему нужен результат дочернего процесса, чтобы продолжить, он вызывает getImportantData()
. Поэтому идея состоит в том, что getImportantData()
блокируется до тех пор, пока не будут вычислены данные.
Однако способ, которым я пользовался, не работает. Я думаю, что это связано с тем, что я предотвращаю выполнение цикла событий с помощью цикла while. И поскольку Event-Loop не выполняет никакое сообщение из дочернего процесса, его можно получить, и, таким образом, условие цикла while не может измениться, сделав его бесконечным циклом.
Конечно, я не хочу использовать этот цикл while. Я бы предпочел сделать node.js "выполнение одной итерации цикла событий, а затем вернуться ко мне". Я бы сделал это несколько раз, пока не получили нужные мне данные, а затем продолжите выполнение, в котором я ушел, вернувшись из получателя.
Я понимаю, что он создает опасность повторного ввода одной и той же функции несколько раз, но модуль, который я хочу использовать в этом, почти ничего не делает в цикле событий, кроме ожидания этого сообщения из дочернего процесса и отправки других сообщений он прогрессирует, поэтому это не должно быть проблемой.
Есть ли способ выполнить только одну итерацию цикла событий в node.js? Или есть другой способ добиться чего-то подобного? Или существует совершенно другой подход к достижению того, что я пытаюсь сделать здесь?
Единственное решение, о котором я мог думать до сих пор, - это изменить расчет таким образом, чтобы я представил еще один процесс. В этом случае будет выполняться процесс вычисления важных данных, процесс вычисления битов данных, для которых важные данные не нужны, и родительский процесс для этих двух, который просто ожидает данных от двух дочерних процессов и комбайнов кусочки, когда они приходят. Поскольку он не должен выполнять какую-либо работу, связанную с вычислениями, он может просто ждать событий из цикла событий (= сообщений) и реагировать на них, пересылать объединенные данные по мере необходимости и хранить фрагменты данных, которые еще не могут быть объединены. Однако это приводит к еще одному процессу и еще большей межпроцессной коммуникации, которая вводит дополнительные накладные расходы, чего я бы хотел избежать.
Изменить
Я вижу, что требуется более подробная информация.
Родительский процесс (пусть он вызывает процесс 1) сам по себе является процессом, порожденным другим процессом (процесс 0), для выполнения некоторой вычислительно-интенсивной работы. На самом деле, он просто выполняет некоторый код, над которым у меня нет контроля, поэтому я не могу заставить его работать асинхронно. То, что я могу сделать (и сделал), - сделать код, который выполняется, регулярно вызывать функцию, чтобы сообщать о ее прогрессе и предоставлять частичные результаты. Этот отчет о ходе работы отправляется обратно в исходный процесс через IPC.
Но в редких случаях частичные результаты неверны, поэтому их нужно модифицировать. Для этого мне нужны некоторые данные, которые я могу вычислить независимо от обычного расчета. Однако этот расчет может занять несколько секунд; таким образом, я запускаю другой процесс (процесс 2) для выполнения этого вычисления и обеспечивает результат для обработки 1 через сообщение IPC. Теперь процесс 1 и 2 с радостью вычисляет там вещи, и, надеюсь, корректирующие данные, рассчитанные по процессу 2, закончены до того, как процесс 1 ему понадобится. Но иногда один из ранних результатов процесса 1 нуждается в исправлении, и в этом случае мне нужно дождаться завершения процесса 2. Блокирование цикла событий процесса 1 теоретически не является проблемой, так как основной процесс (процесс 0) не будет затронут этим. Единственная проблема заключается в том, что, предотвращая дальнейшее выполнение кода в процессе 1, я также блокирую цикл событий, который не позволяет ему получать результат из процесса 2.
Поэтому мне нужно как-то приостановить дальнейшее выполнение кода в процессе 1 без блокировки цикла события. Я надеялся, что был вызов типа process.runEventLoopIteration
, который выполняет итерацию цикла события и затем возвращает.
Затем я бы изменил код следующим образом:
function getImportantData() {
while (importantData === undefined) {
process.runEventLoopIteration();
}
if (importantData === null) {
throw new Error("Data could not be generated.");
} else {
// we should have a proper data now
return importantData;
}
}
тем самым выполняя цикл событий, пока я не получу необходимые данные, но НЕ продолжаю выполнение кода, который называется getImportantData().
В основном, что я делаю в процессе 1, это:
function callback(partialDataMessage) {
if (partialDataMessage.needsCorrection) {
getImportantData();
// use data to correct message
process.send(correctedMessage); // send corrected result to main process
} else {
process.send(partialDataMessage); // send unmodified result to main process
}
}
function executeCode(code) {
run(code, callback); // the callback will be called from time to time when the code produces new data
// this call is synchronous, run is blocking until the calculation is finished
// so if we reach this point we are done
// the only way to pause the execution of the code is to NOT return from the callback
}
Фактическое приложение/реализация/проблема
Мне нужно это поведение для следующего приложения. Если у вас есть лучший подход к достижению этого, не стесняйтесь предлагать его.
Я хочу выполнить произвольный код и получать уведомления о том, какие переменные он меняет, какие функции вызывают, какие исключения происходят и т.д. Мне также необходимо, чтобы эти события в коде отображались для отображения собранной информации в пользовательском интерфейсе рядом с исходным кодом.
Чтобы достичь этого, я обрабатываю код и вставляю в него обратные вызовы. Затем я выполняю код, завершая выполнение в блоке try-catch. Всякий раз, когда обратный вызов вызывается с некоторыми данными об исполнении (например, изменение переменной), я отправляю сообщение в основной процесс, сообщая об этом изменении. Таким образом, пользователь уведомляется о выполнении кода во время его работы. Информация о местоположении для событий, генерируемых этими обратными вызовами, добавляется к вызову обратного вызова во время работы инструментария, поэтому это не проблема.
Проблема возникает, когда возникает исключение. Я также хочу уведомить пользователя об исключениях в тестируемом коде. Поэтому я завернул выполнение кода в try-catch, и все исключения, которые выходят из выполнения, пойманы и отправляются в пользовательский интерфейс. Но расположение ошибок неверно. Объект Error, созданный node.js, имеет полный стек вызовов, поэтому он знает, где он произошел. Но это место, если относительно инструментального кода, поэтому я не могу использовать эту информацию о местоположении как есть, чтобы отобразить ошибку рядом с исходным кодом. Мне нужно преобразовать это местоположение в инструментальном коде в место в исходном коде. Для этого, после настройки кода, я вычисляю исходную карту для сопоставления местоположений в инструментальном коде с местоположениями в исходном коде. Однако этот расчет может занять несколько секунд. Итак, я полагал, я бы начал дочерний процесс для вычисления исходной карты, а выполнение инструментального кода уже начато. Затем, когда возникает исключение, я проверяю, была ли исходная карта уже вычислена, и если она не дождалась завершения вычисления, чтобы исправить местоположение.
Так как код, который будет выполняться и просматриваться, может быть полностью произвольным, я не могу тривиально переписать его как асинхронный. Я знаю только, что он вызывает предоставленный обратный вызов, потому что я использовал код для этого. Я также не могу просто сохранить сообщение и вернуться, чтобы продолжить выполнение кода, вернувшись во время следующего вызова, закончилась ли исходная карта, поскольку продолжение выполнения кода также блокирует цикл события, предотвращая вычисляемый источник карты из когда-либо получаемого в процессе выполнения. Или, если он получен, то только после того, как код для выполнения полностью завершен, что может быть довольно поздно или никогда (если исполняемый код содержит бесконечный цикл). Но прежде чем я получу sourceMap, я не могу отправлять дополнительные обновления о состоянии выполнения. В сочетании, это означает, что я мог бы только отправлять исправленные сообщения о ходе выполнения после завершения кода (что может быть никогда), которое полностью побеждает цель программы (чтобы позволить программисту посмотреть, что делает код, в то время как выполняет).
Временное подчинение управления циклу событий решит эту проблему. Однако это не представляется возможным. Другая идея заключается в том, чтобы ввести третий процесс, который контролирует процесс выполнения и процесс sourceMapGeneration. Он получает сообщения о ходе выполнения процесса выполнения, и если какое-либо из сообщений нуждается в исправлении, он ожидает процесса sourceMapGeneration. Поскольку процессы независимы, процесс управления может хранить полученные сообщения и ждать процесса sourceMapGeneration, пока процесс выполнения продолжает выполняться, и как только он получает исходную карту, он исправляет сообщения и отправляет их все.
Однако это потребует не только другого процесса (накладных расходов), это также означает, что мне нужно еще раз перевести код между процессами, и поскольку код может иметь тысячи строк, что само по себе может занять некоторое время, поэтому я хотел бы перемещать его как можно меньше.
Надеюсь, это объяснит, почему я не могу и не использовал обычный подход "асинхронного обратного вызова".