node.js - 如何避免 Node.js 的 process.send 出现竞争条件?
问题描述
当 Node 中的子进程(由 创建)在父进程拥有消息的事件处理程序child_process.fork()
() 之前向其父进程 () 发送消息时,究竟会发生什么?(至少,似乎必须有某种缓冲。)process.send()
child.on("message",...)
特别是,我面临着似乎不可避免的竞争条件 - 在我完成对 的调用之前,我无法在子进程上安装消息处理程序fork
,但孩子可能会向我(父母)发送消息离开。我有什么保证,假设操作系统进程的交错特别可怕,我会收到我孩子发送的所有消息?
考虑以下示例代码:
父.js:
const child_process = require("child_process");
const child_module = require.resolve("./child");
const run = async () => {
console.log("parent start");
const child = child_process.fork(child_module);
await new Promise(resolve => setTimeout(resolve, 40));
console.log("add handler");
child.on("message", (m) => console.log("parent receive:", m));
console.log("parent end");
};
run();
child.js:
console.log("child start");
process.send("123abc");
console.log("child end");
在上面,我希望通过阻止消息处理程序安装几毫秒来模拟“错误交错”(假设在 fork 之后立即发生上下文切换,并且其他一些进程在此之前运行了一段时间可以再次调度父节点的 node.js 进程)。在我自己的测试中,父母似乎“可靠”地接收到数字 << 40ms(例如 20ms)的消息,但是对于 >35ms 的值,它充其量是不稳定的,对于值 >> 40ms(例如 50 或 60),永远不会收到消息。这些数字有什么特别之处——在我的机器上安排进程的速度有多快?
它似乎与在发送消息之前或之后安装处理程序无关。例如,我观察到以下两个执行都将超时设置为 40 毫秒。请注意,在每一个中,孩子的“结束”消息(表明 process.send() 已经发生)出现在“添加处理程序”之前。在一种情况下,消息被接收,但在下一种情况下,它丢失了。我想,这些进程的标准输出的缓冲可能会导致这些输出歪曲真实的执行——这就是这里发生的事情吗?
Execution A:
parent start
child start
child end
add handler
parent end
parent receive: 123abc
Execution B:
parent start
child start
child end
add handler
parent end
简而言之 - 这种明显的竞争条件是否有解决方案?只要我“尽快”安装处理程序,我似乎就能够“可靠地”接收消息——但我只是走运了,还是有一些保证可以得到?在不依赖运气的情况下,我如何确保此代码始终有效(除了宇宙射线、溢出的咖啡等......)?我似乎无法在 Node 文档中找到有关这应该如何工作的任何细节。
解决方案
当 Node 中的子进程(由 child_process.fork() 创建)在父进程拥有消息的事件处理程序(child.on("message", ...))?(至少,似乎必须有某种缓冲。)
首先,消息从另一个进程到达的事实进入 nodejs 事件队列。它不会被处理,直到当前 nodejs 代码完成它正在做的任何事情并将控制权返回给事件循环,以便它可以处理事件队列中的下一个事件。如果该时刻在该传入事件的任何侦听器之前到达,那么它只是被接收然后被丢弃。消息到达,代码看起来调用任何注册的事件处理程序,如果没有,那么它就完成了。这就像你打电话eventEmitter.emit("someMsg", data)
但没有听众一样"someMsg"
。但是,请继续阅读,您的具体情况是有希望的。
特别是,我面临着似乎不可避免的竞争条件——在完成对 fork 的调用之前,我无法在子进程上安装消息处理程序,但子进程可能会向我(父进程)发送消息马上。我有什么保证,假设操作系统进程的交错特别可怕,我会收到我孩子发送的所有消息?
幸运的是,由于 nodejs 的单线程、事件驱动的特性,这不是问题。您可以在消息到达并被处理之前安装消息处理程序。这是因为即使子进程可能已启动并且可能使用其他 CPU 独立运行或与您的进程交错运行,单线程特性和事件驱动架构可以帮助您解决这个问题。
如果你做这样的事情:
const child = child_process.fork(child_module);
child.on("message", (m) => console.log("parent receive:", m));
然后,您可以保证在有可能处理传入消息之前安装您的消息处理程序,并且您不会错过它。这是因为解释器正忙于运行这两行代码,并且在这两行代码运行之后才将控制权返回给事件循环。因此,在安装处理程序之前,无法处理来自 child_module 的传入消息child.on(...)
。
await
现在,如果您在安装事件处理程序之前故意返回到事件循环,就像您在此处所做的那样,就像您在此处的代码一样:
const run = async () => {
console.log("parent start");
const child = child_process.fork(child_module);
// this await allows events in the event queue to be processed
// while this function is suspended waiting for the await
await new Promise(resolve => setTimeout(resolve, 40));
console.log("add handler");
child.on("message", (m) => console.log("parent receive:", m));
console.log("parent end");
};
run();
然后,您故意使用自己的编码引入了一个竞争条件,只需在此之前安装事件处理程序就可以避免await
这种情况:
const run = async () => {
console.log("parent start");
// no events will be processed before these next three statements run
const child = child_process.fork(child_module);
console.log("add handler");
child.on("message", (m) => console.log("parent receive:", m));
await new Promise(resolve => setTimeout(resolve, 40));
console.log("parent end");
};
run();
推荐阅读
- scala - 如何在 Scala 中获取 .bashrc / .profile 变量
- c++ - 接受函数作为参数的 C++ 模板函数
- c# - 无法使用移动输入翻转精灵
- ruby-on-rails - 为什么我得到:使用邀请的 Hash:Class 的未定义方法“primary_key”!邀请用户时
- javascript - PHP/HTML5:如何检索用户提交的表单?
- python - 如果满足条件,则列出理解以更新值
- python - Python - 绘制一个基本函数
- python - 评估期间的舍入运算
- java - Cordova:在不更改环境变量的情况下指定要使用的 java 路径
- javascript - 循环中声明的函数包含对变量的不安全引用