【问题标题】:Is nesting async calls inside async calls desirable? (Node.js)在异步调用中嵌套异步调用是否可取? (Node.js)
【发布时间】:2017-09-17 00:23:50
【问题描述】:

我正在使用 Node.js,我创建了一个简单的脚本,可以将文件从目录上传到服务器:

var request = require('request');
var file = require('file');
var fs = require('fs');
var path = require('path');


VERSION = '0.1'
CONFIG_FILE = path.join(__dirname, 'etc', 'sender.conf.json');


var config = JSON.parse(
  fs.readFileSync(CONFIG_FILE).toString()
);

var DATA_DIR = __dirname
config['data_dir'].forEach(function(dir) {
  DATA_DIR = path.join(DATA_DIR, dir)
});


console.log('sending data from root directory: ' + DATA_DIR);
file.walk(
  DATA_DIR,
  function(err, dir_path, dirs, files) {
    if(err) {
      return console.error(err);
    } 
    sendFiles(dir_path, files);
  } 
);

function sendFiles(dir_path, files)
{
  files
    .filter(function(file) {
      return file.substr(-5) === '.meta';
    })
    .forEach(function(file) {
      var name = path.basename(file.slice(0, -5));
      sendFile(dir_path, name);
    })
  ; 
} 

function sendFile(dir_path, name)
{
  console.log("reading file start: " + dir_path + "/" + name);
  fs.readFile(
    path.join(dir_path, name + '.meta'),
    function(err, raw_meta) {
      if(err) {
        return console.error(err);
      }
      console.log("reading file done: " + dir_path + "/" + name);
      sendData(
        name,
        JSON.parse(raw_meta),
        fs.createReadStream(path.join(dir_path, name + '.data'))
      );
    }
  );
  console.log("reading file async: " + dir_path + "/" + name);
}

function sendData(name, meta, data_stream)
{ 
  meta['source'] = config['data_source'];

  var req = request.post(
    config['sink_url'],
    function(err, res, body) {
      if(err) {
        console.log(err);
      }
      else {
        console.log(name);
        console.log(meta);
        console.log(body);
      }
    }
  );
  var form = req.form();

  form.append(
    'meta',
    JSON.stringify(meta),
    { 
      contentType: 'application/x-www-form-urlencoded'
    }
  );

  form.append(
    'data',
    data_stream
  );
}

当只使用几个文件运行时,它工作正常。但是当我在有很多文件的目录上运行它时,它会窒息。这是因为它不断创建大量任务以从文件中读取,但从未真正进行读取(因为文件太多)。这可以在输出中观察到:

sending data from root directory: .../data
reading file start: .../data/ac/ad/acigisu-adruire-sabeveab-ozaniaru-fugeef-wemathu-lubesoraf-lojoepe
reading file async: .../data/ac/ad/acigisu-adruire-sabeveab-ozaniaru-fugeef-wemathu-lubesoraf-lojoepe
reading file start: .../data/ac/ab/acodug-abueba-alizacod-ugvut-nucom
reading file async: .../data/ac/ab/acodug-abueba-alizacod-ugvut-nucom
reading file start: .../data/ac/as/acigisu-asetufvub-liwi-ru-mitdawej-vekof
reading file async: .../data/ac/as/acigisu-asetufvub-liwi-ru-mitdawej-vekof
reading file start: .../data/ac/av/ace-avhad-bop-rujan-pehwopa
reading file async: .../data/ac/av/ace-avhad-bop-rujan-pehwopa
...

对于每个文件,在调用fs.readFile 之前立即生成控制台输出"reading file start",并在安排异步读取之后立即生成"reading file async"。但是即使我让它运行很长时间也没有"reading file done" 消息,这意味着甚至可能从未计划过任何文件的读取(这些文件大约为 100 字节,因此一旦安排好,这些读取将可能一次完成)。

这使我想到了以下思考过程。 Node.js 中的异步调用已完成,因为事件循环本身是单线程的,我们不想阻止它。 但是,一旦满足此要求,将进一步的异步调用嵌套到本身嵌套在异步调用等中的异步调用中是否有意义?它会用于任何特定目的吗?此外,如果对单个文件的完整处理仅由同步调用组成,那么由于调度开销并不是真正需要并且可以完全避免的,这难道不是对代码的实际悲观吗?

考虑到上面的思考过程,我的做法是使用this question的解决方案:

  • 异步推送所有文件名到async.queue
  • 通过设置queue.concurrency来限制并行任务的数量
  • 提供完全同步的文件上传处理程序,即同步读取文件内容,完成后,同步向服务器发送POST请求

这是我第一次尝试使用 Node.js 和/或 JavaScript,因此我很可能完全错了(请注意,例如 sync-request package 非常清楚地表明同步调用是不可取的,这是矛盾的根据我上面的思考过程-问题是为什么)。任何关于上述思考过程的有效性以及所提出的解决方案的可行性和最终替代方案的评估都将非常感激。

【问题讨论】:

  • 再想一想,我提出的解决方案并没有真正解决问题:我需要限制将文件添加到队列中,而不是处理文件(最好的方法是什么?) .不过,我会保持文件处理程序同步(异步调用,但不会自行产生更多异步任务)。

标签: node.js asynchronous


【解决方案1】:

== 更新 ==

非常好的article 直接在 Node.js 的文档中详细解释了所有这些。

至于手头的具体问题,确实在文件系统步行者模块的选择上。解决方案是使用例如walk 而不是 file:

@@ -4,7 +4,7 @@


 var request = require('request');
-var file = require('file');
+var walk = require('walk');
 var fs = require('fs');
 var path = require('path');

@@ -24,13 +24,19 @@ config['data_dir'].forEach(function(dir) {


 console.log('sending data from root directory: ' + DATA_DIR);
-file.walk(
-  DATA_DIR,
-  function(err, dir_path, dirs, files) {
-    if(err) {
-      return console.error(err);
-    }
-    sendFiles(dir_path, files);
+var walker = walk.walk(DATA_DIR)
+walker.on(
+  'files',
+  function(dir_path, files, next) {
+    sendFiles(dir_path, files.map(function(stats) { return stats.name; }));
+    next();
+  }
+);
+walker.on(
+  'errors',
+  function(dir_path, node_stats, next) {
+    console.error('file walker:', node_stats);
+    next();
   }
 );

== 原帖 ==

经过更多研究,我将尝试回答我自己的问题。这个答案仍然只是部分解决方案(非常感谢有 Node.js 实际经验的人提供更完整的答案)。

对上述主要问题的简短回答是,确实不仅是可取的,而且几乎总是需要从已经异步的函数中调度更多的异步函数。接下来是长篇大论。

这是因为 Node.js 调度的工作原理:"Everything runs on a different thread except our code."。链接博文下方的讨论中有两个非常重要的 cmets:

  • “Javascript 总是先完成当前正在执行的函数。事件永远不会中断函数。” [Twitchard]
  • “还请注意,它不仅会完成 current 函数,它还会运行到所有同步函数的完成,并且我相信在处理请求回调之前任何与 process.nextTick... 一起排队的东西。” [Tim Oxley]

process.nextTick 的文档中也有一个注释提到这一点:“在处理额外的 I/O 之前,下一个滴答队列在事件循环的每次传递中都完全耗尽。因此,递归设置 nextTick 回调将阻止任何 I/O 发生,就像 while(true); 循环一样。"

所以,总而言之,脚本本身的所有代码都在单线程和单线程上运行。计划运行的异步回调在同一个线程上执行,并且只有在整个当前的下一个滴答队列被耗尽后才会执行。使用异步回调提供了唯一的一点,即可以安排其他一些函数运行。如果文件上传处理程序不会像问题中描述的那样安排任何额外的异步任务,它的执行将阻塞其他所有任务,直到整个文件上传处理程序完成。这是不可取的。

这也解释了为什么输入文件的实际读取永远不会发生(“递归设置 nextTick 回调将阻止任何 I/O 发生” - 见上文)。它最终会在遍历的整个目录层次结构的所有任务都已安排好之后发生。 但是,如果没有进一步研究,我无法回答如何限制计划的文件上传任务的数量(实际上是任务队列的大小)并阻止调度循环,直到其中一些任务被处理(任务队列上的一些空间已被释放)。因此,这个答案仍然不完整。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2012-07-15
  • 1970-01-01
  • 1970-01-01
  • 2016-07-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-15
相关资源
最近更新 更多