首页 > 解决方案 > 如何避免 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.js

解决方案


当 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();

推荐阅读