【问题标题】:child_process spawn Race condition possibility in nodejschild_process spawn nodejs中的竞争条件可能性
【发布时间】:2015-04-18 19:33:16
【问题描述】:

我开始学习和使用 node,我喜欢它,但我不确定某些功能是如何工作的。也许你可以帮我解决一个这样的问题:

我想根据 rest 命令从我的节点服务器生成本地脚本和程序。查看 fs 库,我看到了下面的示例,该示例说明了如何生成子进程并在其上添加一些管道/事件处理程序。

var spawn = require('child_process').spawn,
    ps    = spawn('ps', ['ax']),
    grep  = spawn('grep', ['ssh']);

ps.stdout.on('data', function (data) {
  grep.stdin.write(data);
});

ps.stderr.on('data', function (data) {
  console.log('ps stderr: ' + data);
});

ps.on('close', function (code) {
  if (code !== 0) {
    console.log('ps process exited with code ' + code);
  }
  grep.stdin.end();
});

grep.stdout.on('data', function (data) {
  console.log('' + data);
});

grep.stderr.on('data', function (data) {
  console.log('grep stderr: ' + data);
});

grep.on('close', function (code) {
  if (code !== 0) {
    console.log('grep process exited with code ' + code);
  }
});

让我感到奇怪的是,我不明白如何保证在程序开始运行之前注册事件处理程序代码。这不像您运行一个“恢复”功能来启动孩子。这不是比赛条件吗?当然,条件会非常小,几乎不会受到影响,因为它会在之后剪掉这么短的代码,但如果是这样的话,我宁愿不要因为良好的习惯而以这种方式编码。

所以: 1)如果不是比赛条件,为什么? 2)如果这是一个竞争条件,我怎么能以正确的方式编写它?

感谢您的宝贵时间!

【问题讨论】:

    标签: node.js race-condition fs


    【解决方案1】:

    这不是竞争条件。 Node.js 是单线程的,并以先到先得的方式处理事件。新事件放在事件循环的末尾。 Node 将以同步方式执行您的代码,其中一部分将涉及设置事件发射器。当这些事件发射器发出事件时,它们将被放在队列的末尾,并且在 Node 完成执行其当前正在处理的任何代码之前不会被处理,这恰好是注册侦听器的相同代码。因此,监听器总是会在事件处理之前被注册。

    【讨论】:

    • 感谢 Yuri,这对于我询问从主服务器代码生成 spawn 的情况是有道理的。只是为了得到更多的澄清,如果我生了一个孩子,然后又生了孩子,你的逻辑仍然成立吗?
    • 没问题。我不明白为什么它不会。
    • 我不明白这个。 Node 只能控制它自己的线程模型,它无法控制它产生的子进程。当我单步执行spawnsa 子进程的一些代码并检查spawn 返回的值时,一旦我到达下一条语句,它就已经有一个进程ID,这意味着该进程在我的任何处理程序之前运行注册(至少在 Windows 上)。这意味着你错了。
    • 是的,您的子进程肯定正在运行,并且可能正在发出事件,但这些事件还没有被处理。以 OP 为例,只有在执行完当前上下文(在其中生成子进程并设置事件处理程序)之后,Node 才会开始处理子进程发出的事件。
    【解决方案2】:

    鉴于已接受答案的 cmets 存在轻微冲突和模棱两可,下面的示例和输出告诉我两件事:

    1. 子进程(指spawn 返回的节点对象)不会发出任何事件,即使真正的底层进程正在运行/正在执行。
    2. IPC 的管道在子进程执行之前设置。

    两者都很明显。冲突是w.r.t。 OP问题的解释:-

    实际上“是”,如果需要考虑真正的子进程的副作用,这就是数据竞争条件的缩影。但是“不”,就 IPC 管道而言,没有数据竞赛。数据被写入缓冲区并在上下文完成时(如前所述)以(更大的)blob 形式检索,从而允许事件循环继续。

    下面看到的第一个数据事件不是将 1 个而是 5 个块推送到我们阻塞时由子进程写入标准输出。因此没有任何内容丢失。

    样本:

    let t = () => (new Date()).toTimeString().split(' ')[0]
    let p = new Promise(function (resolve, reject) {
      console.log(`[${t()}|info] spawning`);
    
      let cp = spawn('bash', ['-c', 'for x in `seq 1 1 10`; do printf "$x\n"; sleep 1; done']);
      let resolved = false;
    
      if (cp === undefined)
        reject();
    
      cp.on('error', (err) => {
        console.log(`error: ${err}`);
        reject(err);
      });
    
      cp.stdout.on('data', (data) => {
        if (!resolved) {
          console.log(`[${t()}|info] spawn succeeded`);
          resolved = true;
          resolve();
        }
        process.stdout.write(`[${t()}|data] ${data}`);
      });
    
      let ts = parseInt(Date.now() / 1000);
      while (parseInt(Date.now() / 1000) - ts < 5) {
        // waste some cycles in the current context
        ts--; ts++;
      }
    
      console.log(`[${t()}|info] synchronous time wasted`);
    });
    Promise.resolve(p);
    

    输出:

    [18:54:18|info] spawning
    [18:54:23|info] synchronous time wasted
    [18:54:23|info] spawn succeeded
    [18:54:23|data] 1
    2
    3
    4
    5
    [18:54:23|data] 6
    [18:54:24|data] 7
    [18:54:25|data] 8
    [18:54:26|data] 9
    [18:54:27|data] 10
    

    【讨论】:

    • 如果您将同步时间浪费移到 附加 stdout 和 stderr 处理程序的 .on(...) 调用之前,这将更有说服力,以证明即使有当子进程正在输出数据时,尚未附加任何处理程序,该数据仍将被缓冲并最终传递给on('data', ...)处理程序。
    • 有趣且令我惊讶的是,我注意到即使您异步浪费时间,也可以通过仅在setTimeout 回调中添加'data' 处理程序来实现。
    猜你喜欢
    • 1970-01-01
    • 2013-04-13
    • 2017-07-17
    • 1970-01-01
    • 2021-12-06
    • 1970-01-01
    • 2019-09-09
    • 1970-01-01
    • 2013-05-06
    相关资源
    最近更新 更多