2

我想对任意大的文件集执行一些任意昂贵的工作。我想实时报告进度,然后在处理完所有文件后显示结果。如果没有与我的表达式匹配的文件,我想抛出一个错误。

想象一下编写一个测试框架来加载所有测试文件,执行它们(不按特定顺序),实时报告进度,然后在所有测试完成后显示汇总结果。

用阻塞语言(例如 Ruby)编写这段代码非常简单。

事实证明,我在节点中执行这个看似简单的任务时遇到了麻烦,同时也真正利用了异步的、基于事件的 IO。

我的第一个设计是按顺序执行每个步骤。

  1. 加载所有文件,创建要处理的文件集合
  2. 处理集合中的每个文件
  3. 处理完所有文件后报告结果

这种方法确实有效,但对我来说似乎不太正确,因为它会导致我的程序中计算成本更高的部分等待所有文件 IO 完成。这难道不是 Node 旨在避免的那种等待吗?

我的第二个设计是处理每个文件,因为它是在磁盘上异步找到的。为了争论,让我们想象一个看起来像这样的方法:

eachFileMatching(path, expression, callback) {
  // recursively, asynchronously traverse the file system,
  // calling callback every time a file name matches expression.
}

这个方法的消费者看起来像这样:

eachFileMatching('test/', /_test.js/, function(err, testFile) {
  // read and process the content of testFile
});

虽然这种设计感觉像是一种使用 IO 的非常“节点”的方式,但它存在两个主要问题(至少在我推测的错误实现中):

  1. 我不知道什么时候所有的文件都被处理了,所以我不知道什么时候组装和发布结果。
  2. 因为文件读取是非阻塞的,并且是递归的,所以我正在努力了解如何知道是否没有找到文件。

我希望我只是做错了什么,并且其他人使用一些相当简单的策略来使第二种方法起作用。

尽管这个例子使用了一个测试框架,但我还有很多其他项目遇到了同样的问题,我想任何人都会编写一个相当复杂的应用程序来访问节点中的文件系统。

4

3 回答 3

1

“读取和处理 testFile 的内容”是什么意思?

我不明白为什么您不知道何时处理所有文件。你不使用流吗?一个流有几个事件,而不仅仅是data. 如果您处理end事件,那么您将知道每个文件何时完成。

例如,您可能有一个list文件名,为每个文件设置处理,然后当您收到end事件时,从列表中删除文件名。当列表为空时,您就完成了。或者创建一个包含名称和完成状态的 FileName 对象。当您收到end事件时,更改状态并减少文件名计数器。当计数器变为零时,您就完成了,或者如果您不确定可以扫描所有 FileName 对象以确保它们的状态已完成。

您可能还有一个计时器,它会定期检查计数器,如果它在一段时间内没有变化,则报告处理可能会停留在状态未完成的 FileName 对象上。

...我刚刚在另一个问题中遇到了这种情况,并且接受的答案(加上 github 链接)很好地解释了它。检查事件驱动代码的循环?

于 2011-02-24T20:40:06.973 回答
1

事实证明,我能够构建的最小工作解决方案比我希望的要复杂得多。

以下是对我有用的代码。它可能可以在这里和那里进行清理或使其更具可读性,我对这样的反馈不感兴趣。

如果有一个明显不同的方法来解决这个问题,那就是更简单和/或更有效,我很想听听它。让我感到惊讶的是,要解决这个看似简单的要求需要如此大量的代码,但也许这就是为什么有人发明了阻塞 io 的原因?

复杂性实际上在于满足以下所有要求:

  • 处理文件,因为它们被发现
  • 知道搜索何时完成
  • 知道是否找不到文件

这是代码:

/**
 * Call fileHandler with the file name and file Stat for each file found inside
 * of the provided directory.
 *
 * Call the optionally provided completeHandler with an array of files (mingled
 * with directories) and an array of Stat objects (one for each of the found
 * files.
 *
 * Following is an example of a simple usage:
 *
 *   eachFileOrDirectory('test/', function(err, file, stat) {
 *     if (err) throw err;
 *     if (!stat.isDirectory()) {
 *       console.log(">> Found file: " + file);
 *     }
 *   });
 *
 * Following is an example that waits for all files and directories to be 
 * scanned and then uses the entire result to do something:
 *
 *   eachFileOrDirectory('test/', null, function(files, stats) {
 *     if (err) throw err;
 *     var len = files.length;
 *     for (var i = 0; i < len; i++) {
 *       if (!stats[i].isDirectory()) {
 *         console.log(">> Found file: " + files[i]);
 *       }
 *     }
 *   });
 */
var eachFileOrDirectory = function(directory, fileHandler, completeHandler) {
  var filesToCheck = 0;
  var checkedFiles = [];
  var checkedStats = [];

  directory = (directory) ? directory : './';

  var fullFilePath = function(dir, file) {
    return dir.replace(/\/$/, '') + '/' + file;
  };

  var checkComplete = function() {
    if (filesToCheck == 0 && completeHandler) {
      completeHandler(null, checkedFiles, checkedStats);
    }
  };

  var onFileOrDirectory = function(fileOrDirectory) {
    filesToCheck++;
    fs.stat(fileOrDirectory, function(err, stat) {
      filesToCheck--;
      if (err) return fileHandler(err);
      checkedFiles.push(fileOrDirectory);
      checkedStats.push(stat);
      fileHandler(null, fileOrDirectory, stat);
      if (stat.isDirectory()) {
        onDirectory(fileOrDirectory);
      }
      checkComplete();
    });
  };

  var onDirectory = function(dir) {
    filesToCheck++;
    fs.readdir(dir, function(err, files) {
      filesToCheck--;
      if (err) return fileHandler(err);
      files.forEach(function(file, index) {
        file = fullFilePath(dir, file);
        onFileOrDirectory(file);
      });
      checkComplete();
    });
  }

  onFileOrDirectory(directory);
};
于 2011-02-27T03:07:54.667 回答
0

这样做的 2 种方法,首先并且可能连续考虑会像

var files = [];
doFile(files, oncomplete);

function doFile(files, oncomplete) {
  if (files.length === 0) return oncomplete();
  var f = files.pop();
  processFile(f, function(err) {
    // Handle error if any
    doFile(files, oncomplete); // Recurse
  });
};

function processFile(file, callback) {
  // Do whatever you want to do and once 
  // done call the callback
  ...
  callback();
};

第二种方式,让我们称之为并行是相似的,总结如下:

var files = [];
doFiles(files, oncomplete);

function doFiles(files, oncomplete) {
  var exp = files.length;
  var done = 0;
  for (var i = 0; i < exp; i++) {
    processFile(files[i], function(err) {
      // Handle errors (but still need to increment counter)
      if (++done === exp) return oncomplete();      
    });
  }
};

function processFile(file, callback) {
  // Do whatever you want to do and once 
  // done call the callback
  ...
  callback();
};

现在似乎很明显您应该使用第二种方法,但是您会发现对于 IO 密集型操作,在并行化时您并没有真正获得任何性能提升。第一种方法的一个缺点是递归可能会破坏您的堆栈跟踪。

肿瘤坏死因子

圭多

于 2011-02-25T00:34:32.420 回答