Запустить цикл событий 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 не может измениться, сделав его бесконечным циклом.

Конечно, я не хочу использовать этот цикл 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, пока процесс выполнения продолжает выполняться, и как только он получает исходную карту, он исправляет сообщения и отправляет их все.

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

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

Ответ 1

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

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

Вот функция 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 проблемы одинаково. Я рад, что нам удалось выработать то, что сработало для вас. Уверенно, это был довольно интересный вопрос.

Ответ 2

Правило асинхронного программирования заключается в том, что после ввода асинхронного кода вы должны продолжать использовать асинхронный код. Хотя вы можете продолжать повторять функцию снова и снова через 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
    }
});

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

Ответ 3

То, что вы используете, - очень распространенный сценарий, с которым часто сталкиваются опытные программисты, которые начинают с 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.

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

Обычно это делается в node с обратным вызовом

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
})

Ответ 4

Ваш вопрос (обновленный) очень интересен, он, по-видимому, тесно связан с проблемой, с которой я сталкивался с асинхронными исключениями. (Также Брэндон и Ihad интересная дискуссия со мной об этом! Это маленький мир)

См. этот вопрос о том, как асинхронно перехватывать исключения. Основная концепция заключается в том, что вы можете использовать (предполагая nodejs 0.8+) nodejs domains, чтобы ограничить область исключения.

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

Вы можете найти соответствующий код в связанном вопросе. Использование - это что-то вроде:

atry(function() {
    setTimeout(function(){
        throw "something";
    },1000);
}).catch(function(err){
    console.log("caught "+err);
});

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

Удачи!