Node 生成的子进程何时真正开始?

when does Node spawned child process actually start?

documentation for Node's Child Process spawn() function以及我在别处看到的例子中,模式是调用spawn()函数,然后然后来设置返回的 ChildProcess 对象上的一堆处理程序。例如,这是该文档页面上给出的 spawn() 的第一个示例:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

spawn() 函数本身在第二行被调用。我的理解是 spawn() 异步启动子进程。来自文档:

The child_process.spawn() method spawns a new process using the given command, with command line arguments in args.

但是,上面脚本的以下几行继续为进程设置各种处理程序,因此假设进程在 spawn()在第 2 行调用,其他内容发生在后续行。我知道 JavaScript/Node 是单线程的。但是,操作系统不是单线程的,天真地会读到 spawn() 调用告诉操作系统立即生成进程(此时,不幸的是, OS 可能在执行下一行节点代码之前挂起父节点进程和 run/complete 子进程。

但必须是在当前 JavaScript 函数完成(或更一般地,调用当前函数的当前 JavaScript 事件处理程序完成)之前,进程不会真正产生,对吧?

这似乎是一件非常重要的事情。为什么它不在子进程文档页面中说明?是否有一些压倒一切的 Node 原则使得无需明确说明?

新进程的产生立即开始(它被移交给 OS 以实际启动进程并使其运行)。使用 .spawn() 启动新进程是异步和非阻塞的。因此,它将使用 OS 并立即 return 启动操作。您可能认为这就是为什么可以在它 return 之后设置事件处理程序的原因(因为该过程尚未完成启动)。好吧,是的,不是。它可能还没有完成启动新进程,但这不是它没问题的主要原因。

没关系,因为 node.js 运行 通过单线程事件队列处理所有事件。因此,只有在您的代码执行完毕并且 return 将控制权交还给系统之后,才能处理来自新产生的进程的事件。只有这样它才能处理事件队列中的下一个事件并触发您正在为其注册处理程序的事件之一。

或者,换句话说,none 来自其他进程的事件是先发制人的。它们不会t/can 中断您现有的 Javascript 代码。因此,由于您仍在 运行ning 您的 Javascript 代码,这些事件还不能 运行。相反,它们位于事件队列中,直到您的 Javascript 代码完成,然后解释器可以从事件队列中获取下一个事件和 运行 与之关联的回调。同样,该回调 运行s 直到它 returns 返回给解释器,然后解释器可以获得下一个事件和 运行 它的回调等等......

这就是 node.js 被称为事件驱动系统的原因。

因此,做这种类型的结构是完全没问题的:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});
那些 dataclose 事件中的

None 可以执行它们的回调,直到您的代码完成并且 return 将控制权交还给系统。因此,像您一样设置这些事件处理程序是绝对安全的。即使新生成的进程 运行 正在运行并立即生成事件,这些事件也只会留在事件队列中,直到您的 Javascript 完成它正在做的事情(包括设置您的事件处理程序)。

现在,如果您将事件处理程序的设置延迟到事件循环的某个未来滴答(如下所示),例如 setTimeout(),那么您可能会错过一些事件:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

setTimeout(() => {    
    ls.stdout.on('data', (data) => {
      console.log(`stdout: ${data}`);
    });

    ls.stderr.on('data', (data) => {
      console.error(`stderr: ${data}`);
    });

    ls.on('close', (code) => {
      console.log(`child process exited with code ${code}`);
    });
}, 10);

这里您没有立即将事件处理程序设置为事件循环的同一滴答的一部分,而是在短暂的延迟之后。因此,在您安装事件处理程序之前,一些事件可能会从事件循环中得到处理,并且您可能会错过其中一些事件。显然,你永远不会(故意)这样做,但我只是想表明代码 运行ning 在事件循环的同一刻度上没有问题,但是代码 运行ning在事件循环的某些未来滴答声中可能会出现缺少事件的问题。

这是为了跟进jfriend00的回答,解释一下它帮助我理解的内容,以防对其他人有所帮助。我知道 JavaScript/Node 的事件驱动性质。 jfriend00 的解释让我明白了一个想法,即事件可以发生并且 Node 可以知道它发生了,但它实际上并没有决定在下一个 tick 之前通知哪个处理程序。例如,如果 spawn() 调用完全失败(例如,命令不存在),Node 显然会立即知道。我的想法是,它会在下一个滴答时立即将适当的处理程序排队到 运行。但我现在明白的是,它将"raw event"(即生成失败的事实,以及关于它的任何细节)放入队列中,然后在下一个滴答中确定 并调用适当的处理程序。其他事件也是如此,例如从进程接收输出等。事件已保存,但事件的适当处理程序仅在下一个 tick 运行s 时才确定,因此在上一个 tick 上分配的处理程序,在 spawn() 之后,将被调用。