Запустите цикл событий NodeJS / дождитесь завершения дочернего процесса
Сначала я попробовал общее описание проблемы, а затем немного подробнее, почему обычные подходы не работают. Если вы хотите прочитать эти абстрактные объяснения, продолжайте. В конце я объясню большую проблему и конкретное приложение, поэтому, если вы предпочитаете читать это, перейдите к "Актуальное приложение".
Я использую дочерний процесс 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 не может измениться, что делает его бесконечным циклом.
Конечно, я не очень хочу использовать этот цикл. То, что я бы предпочел сделать, это сказать 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, имеет полный стек вызовов, поэтому он знает, где он произошел. Но это местоположение относительно инструментального кода, поэтому я не могу использовать эту информацию о местоположении как есть, чтобы отобразить ошибку рядом с исходным кодом. Мне нужно преобразовать это местоположение в инструментальном коде в местоположение в исходном коде. Для этого после инструментирования кода я рассчитываю исходную карту для сопоставления местоположений в инструментальном коде с местоположениями в исходном коде. Однако этот расчет может занять несколько секунд. Итак, я решил, что запустил бы дочерний процесс для вычисления исходной карты, в то время как выполнение инструментированного кода уже началось. Затем, когда возникает исключение, я проверяю, была ли уже рассчитана исходная карта, и, если это не так, я жду окончания расчета, чтобы иметь возможность исправить местоположение.
Поскольку код, который должен выполняться и отслеживаться, может быть совершенно произвольным, я не могу просто переписать его как асинхронный. Я только знаю, что он вызывает предоставленный обратный вызов, потому что я дал код для этого. Я также не могу просто сохранить сообщение и вернуться, чтобы продолжить выполнение кода, проверяя во время следующего вызова, завершена ли исходная карта, потому что продолжение выполнения кода также заблокировало бы цикл обработки событий, предотвращая вычисление источника карта из когда-либо полученных в процессе выполнения. Или, если он получен, то только после того, как код для выполнения полностью завершен, что может быть довольно поздно или никогда (если код для выполнения содержит бесконечный цикл). Но прежде чем я получу исходную карту, я не могу отправить дальнейшие обновления о состоянии выполнения. В совокупности это означает, что я смогу отправлять исправленные сообщения о ходе выполнения только после завершения выполнения кода (что может быть никогда), что полностью противоречит цели программы (чтобы позволить программисту наблюдать за тем, что делает код, пока он выполняет).
Временная передача управления циклу событий решит эту проблему. Однако это не представляется возможным. Другая идея, которую я имею, состоит в том, чтобы представить третий процесс, который контролирует как процесс выполнения, так и процесс sourceMapGeneration. Он получает сообщения о ходе выполнения от процесса выполнения, и если какое-либо из сообщений нуждается в исправлении, он ожидает процесса sourceMapGeneration. Поскольку процессы независимы, управляющий процесс может сохранять полученные сообщения и ожидать процесса sourceMapGeneration, пока процесс выполнения продолжает выполняться, и, как только он получает карту источника, он исправляет сообщения и отправляет их все.
Однако это потребует не только еще одного процесса (накладных расходов), это также означает, что мне придется еще раз передавать код между процессами, и так как код может содержать тысячи строк, что само по себе может занять некоторое время, поэтому я хотел бы переместить его вокруг как можно меньше.
Я надеюсь, что это объясняет, почему я не могу и не использовал обычный подход "асинхронного обратного вызова".
4 ответа
Добавление третьего (:)) решения вашей проблемы после того, как вы уточнили, какое поведение вы ищете, я предлагаю использовать Fibers.
Волокна позволяют вам выполнять сопрограммы в nodejs. Сопрограммы - это функции, которые позволяют использовать несколько точек входа / выхода. Это означает, что вы сможете вернуть контроль и возобновить его, как пожелаете.
Вот sleep
Функция из официальной документации, которая делает именно это, спит в течение определенного количества времени и выполняет действия.
function sleep(ms) {
var fiber = Fiber.current;
setTimeout(function() {
fiber.run();
}, ms);
Fiber.yield();
}
Fiber(function() {
console.log('wait... ' + new Date);
sleep(1000);
console.log('ok... ' + new Date);
}).run();
console.log('back in main');
Вы можете поместить код, выполняющий ожидание ресурса, в функцию, заставляя его работать, а затем снова запускаться после выполнения задачи.
Например, адаптируя ваш пример из вопроса:
var pausedExecution, importantData;
function getImportantData() {
while (importantData === undefined) {
pausedExecution = Fiber.current;
Fiber.yield();
pausedExecution = undefined;
}
if (importantData === null) {
throw new Error("Data could not be generated.");
} else {
// we should have proper data now
return importantData;
}
}
function callback(partialDataMessage) {
if (partialDataMessage.needsCorrection) {
var theData = 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) {
// setup child process to calculate the data
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!");
}
if (pausedExecution) {
// execution is waiting for the data
pausedExecution.run();
}
});
// wrap the execution of the code in a Fiber, so it can be paused
Fiber(function () {
runCodeWithCallback(code, callback); // the callback will be called from time to time when the code produces new data
// this callback is synchronous and blocking,
// but it will yield control to the event loop if it has to wait for the child-process to finish
}).run();
}
Удачи! Я всегда говорю, что лучше решить одну проблему 3 способами, чем решать 3 проблемы одинаково. Я рад, что мы смогли разработать что-то, что сработало для вас. По общему признанию, это был довольно интересный вопрос.
Правило асинхронного программирования: после ввода асинхронного кода вы должны продолжать использовать асинхронный код. Хотя вы можете продолжать вызывать функцию снова и снова через setImmediate
или что-то в этом роде, у вас все еще есть проблема, которую вы пытаетесь return
из асинхронного процесса.
Не зная больше о вашей программе, я не могу сказать вам точно, как вы должны структурировать ее, но в целом способ "вернуть" данные из процесса, который включает асинхронный код, состоит в том, чтобы передать обратный вызов; возможно, это поставит вас на правильный путь:
function getImportantData(callback) {
importantDataCalculator = fork("./runtime");
importantDataCalculator.on("message", function (msg) {
if (msg.type === "result") {
callback(null, msg.data);
} else if (msg.type === "error") {
callback(new Error("Data could not be generated."));
} else {
callback(new Error("Unknown message from sourceMapGenerator!"));
}
});
}
Затем вы бы использовали эту функцию следующим образом:
getImportantData(function(error, data) {
if (error) {
// handle the error somehow
} else {
// `data` is the data from the forked process
}
});
Я расскажу об этом чуть более подробно в одном из моих скринкастов " Мышление асинхронно".
То, с чем вы сталкиваетесь, является очень распространенным сценарием, с которым часто сталкиваются опытные программисты, начинающие с nodejs.
Ты прав. Вы не можете сделать это так, как вы пытаетесь (цикл).
Основной процесс в node.js является однопоточным, и вы блокируете цикл обработки событий.
Самый простой способ решить это что-то вроде:
function getImportantData() {
if(importantData === undefined){ // not set yet
setImmediate(getImportantData); // try again on the next event loop cycle
return; //stop this attempt
}
if (importantData === null) {
throw new Error("Data could not be generated.");
} else {
// we should have a proper data now
return importantData;
}
}
Что мы делаем, так это то, что функция повторно пытается обработать данные на следующей итерации цикла событий, используя setImmediate
,
Это вводит новую проблему, однако, ваша функция возвращает значение. Поскольку он не будет готов, возвращаемое вами значение не определено. Таким образом, вы должны кодировать реактивно. Вы должны сообщить своему коду, что делать, когда поступают данные.
Обычно это делается в узле с обратным вызовом
function getImportantData(err,whenDone) {
if(importantData === undefined){ // not set yet
setImmediate(getImportantData.bind(null,whenDone)); // try again on the next event loop cycle
return; //stop this attempt
}
if (importantData === null) {
err("Data could not be generated.");
} else {
// we should have a proper data now
whenDone(importantData);
}
}
Это можно использовать следующим образом
getImportantData(function(err){
throw new Error(err); // error handling function callback
}, function(data){ //this is whenDone in our case
//perform actions on the important data
})
Ваш вопрос (обновленный) очень интересен, похоже, он тесно связан с проблемой, возникшей у меня с асинхронным перехватом исключений. (Также у Брэндона и меня была интересная дискуссия об этом! Это маленький мир)
Посмотрите этот вопрос о том, как асинхронно перехватывать исключения. Ключевая концепция заключается в том, что вы можете использовать (предположим, nodejs 0.8+) узлы nodejs, чтобы ограничить область действия исключения.
Это позволит вам легко определить местоположение исключения, поскольку вы можете окружать асинхронные блоки atry/catch
, Я думаю, что это должно решить большую проблему здесь.
Вы можете найти соответствующий код в связанном вопросе. Использование что-то вроде:
atry(function() {
setTimeout(function(){
throw "something";
},1000);
}).catch(function(err){
console.log("caught "+err);
});
Так как у вас есть доступ к сфере atry
вы можете получить трассировку стека, которая позволит вам пропустить более сложное использование карты источника.
Удачи!