运行NodeJS事件循环/等待子进程完成

时间:2013-05-04 19:23:42

标签: node.js event-loop

我首先尝试对问题进行一般性描述,然后详细说明为什么通常的方法不起作用。如果你想阅读这些抽象的解释继续下去。最后,我解释了更大的问题和具体的应用,所以如果你想读它,跳转到"实际应用"。

我正在使用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循环阻止事件循环执行。并且由于事件循环不执行,因此无法接收来自子进程的消息,因此while循环的条件不会改变,从而使其成为无限循环。

当然,我并不是真的想要使用这种while循环。我宁愿做的是告诉node.js"执行事件循环的一次迭代,然后回到我身边#34;。我会反复这样做,直到收到我需要的数据,然后通过从getter返回继续执行我离开的位置。

我意识到他冒了几次重新进入同一个函数的危险,但是我想要使用它的模块在事件循环中几乎什么也没做,除了等待来自子进程的这个消息并发送其他消息报告它的进步,所以不应该是一个问题。

有没有办法在Node.js中只执行一次事件循环迭代?还是有另一种方法来实现类似的东西吗?或者是否有一种完全不同的方法来实现我在这里尝试做的事情?

到目前为止,我能想到的唯一解决方案是改变计算方式,以便引入另一个流程。在这种情况下,将会有计算重要数据的过程,一个计算不需要重要数据的数据位的过程以及这两个过程的父过程,它只是等待来自两个子进程和组合的数据他们到达时的碎片。由于它本身不需要进行任何计算密集型工作,因此它可以等待来自事件循环(=消息)的事件并对它们作出反应,根据需要转发组合数据并存储尚未组合的数据片段。 然而,这引入了另一个进程和更多的进程间通信,这引入了更多的开销,我想避免。

修改

我看到需要更多细节。

父进程(让它称之为进程1)本身就是由另一个进程(进程0)生成的进程,用于执行一些计算密集型工作。实际上,它只执行一些我无法控制的代码,所以我不能让它异步工作。我能做(并且已经完成)的是使定期执行的代码调用函数来报告它的进度并提供部分结果。然后通过IPC将此进度报告发送回原始流程。

但在极少数情况下,部分结果不正确,因此必须进行修改。为此,我需要一些我可以独立于正常计算计算的数据。但是,这种计算可能需要几秒钟;因此,我开始另一个过程(过程2)进行此计算,并通过IPC消息将结果提供给过程1。现在,流程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 
}

实际应用/实施/问题

我需要以下应用程序的此行为。如果您有更好的方法来实现这一点,请随意提出它。

我想执行任意代码,并通知它更改了哪些变量,调用了哪些函数,发生了什么异常等等。我还需要在代码中找到这些事件的位置,以便能够在UI中显示收集的信息在原始代码旁边。

为实现这一目标,我会检测代码并将回调插入其中。然后我执行代码,将执行包装在try-catch块中。每当使用一些关于执行的数据(例如变量更改)调用回调时,我就向主进程发送一条消息,告诉它有关更改的信息。这样,当用户正在运行时,将通知用户代码的执行。这些回调生成的事件的位置信息会在检测期间添加到回调调用中,因此这不是问题。

发生异常时出现问题。我还想通知用户测试代码中的异常。因此,我将代码的执行包装在try-catch中,并且捕获任何从执行中获取的异常并将其发送到用户界面。但错误的位置不正确。 node.js创建的Error对象具有完整的调用堆栈,因此它知道它发生的位置。但是这个位置如果相对于检测代码,那么我就不能按原样使用这个位置信息,来显示原始代码旁边的错误。我需要将已检测代码中的此位置转换为原始代码中的位置。为此,在检测代码后,我计算source map以将检测代码中的位置映射到原始代码中的位置。但是,此计算可能需要几秒钟。所以,我想,我会启动一个子进程来计算源映射,而已经启动了已检测代码的执行。然后,当发生异常时,我会检查是否已经计算了源地图,如果它还没有等待计算完成以便能够纠正位置。

由于要执行和监视的代码可以完全任意,我不能轻易地将其重写为异步。我只知道它调用提供的回调,因为我检测了代码来执行此操作。我也不能只存储消息并返回继续执行代码,在下次调用期间检查源映射是否已完成,因为继续执行代码也会阻塞事件循环,阻止计算源从执行过程中收到的地图。或者如果它被接收,那么只有在执行的代码完全完成之后,这可能很晚或从不(如果执行的代码包含无限循环)。但在收到sourceMap之前,我无法发送有关执行状态的进一步更新。结合起来,这意味着我只能在执行代码完成后(可能永远不会)发送纠正的进度消息,这完全违背了程序的目的(使程序员能够看到代码的作用,同时它执行)。

暂时放弃对事件循环的控制可以解决这个问题。但是,这似乎不可能。我的另一个想法是引入一个控制执行过程和sourceMapGeneration过程的第三个过程。它从执行过程接收进度消息,如果任何消息需要更正,则等待sourceMapGeneration进程。由于进程是独立的,控制进程可以存储接收的消息并在执行过程继续执行时等待sourceMapGeneration进程,并且一旦收到源映射,它就会纠正消息并将所有消息发送出去。

但是,这不仅需要另一个进程(开销),这也意味着我必须在进程之间再次传输代码,因为代码本身可能需要花费一些时间才能拥有数千行,所以我希望尽可能少地移动它。

我希望这可以解释,为什么我不能也不会使用通常的"异步回调"方法

4 个答案:

答案 0 :(得分:6)

在您澄清了我寻求的行为之后,为您的问题添加第三个​​(:))解决方案我建议使用Fibers

Fibers允许您在nodejs中执行co-routines。协同程序是允许多个进入/退出点的功能。这意味着您将能够控制并随意恢复它。

以下是官方文档中的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个问题更好。我很高兴我们能够找到适合你的东西。不可否认,这是一个非常有趣的问题。

答案 1 :(得分:5)

异步编程规则是,一旦输入异步代码,就必须继续使用异步代码。虽然您可以继续通过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
    }
});

我在我的一个截屏视频Thinking Asynchronously中详细讨论了这个问题。

答案 2 :(得分:2)

您遇到的是一个非常常见的情况,那些以nodej开头的熟练程序员经常会遇到困难。

你是对的。你不能按照你的方式(循环)这样做。

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

答案 3 :(得分:1)

你的问题(更新)非常有趣,它似乎与我异步捕获异常的问题密切相关。 (同时Brandon和Ihad就我的一个有趣的讨论!这是一个小世界)

请参阅this question,了解如何异步捕获异常。关键概念是您可以使用(假设nodejs 0.8+)nodejs domains来约束异常的范围。

这样您就可以轻松获取异常的位置,因为您可以使用atry/catch包围异步块。我认为这应该解决更大的问题。

您可以在链接的问题中找到相关代码。用法类似于:

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

由于您可以访问atry的范围,因此可以在那里获得堆栈跟踪,这样可以跳过更复杂的源映射用法。

祝你好运!