我试图读取并解析一个大的csv文件,对于每一行,我必须做一些异步计算,并在操作完成后增加计数器。所以我创建了一个Promise p
并尝试将许多.then(xxx)
链接起来,并在csv结束时读取它的最终.then(yyy)
以输出计数。
但是这个数字并没有加起来。但是如果我p = p.then(xxx)
和p = p.then(yyy)
这个数字会加起来(对于较小的csv文件)但我有时会遇到内存泄漏(对于大型csv文件)。
我做错了吗?
var fs = require('fs')
const csv = require('fast-csv');
var Promise = require('bluebird')
var count = 0;
var actual = 0;
let p = Promise.resolve();
const stream = fs.createReadStream(`/Users/ssmlee/Desktop/KingKong_Sims_5M.txt`);
const csvStream = csv({
delimiter: ';'
})
.on('data', (row) => {
count++
if (count % 10000 === 0) {
console.log(count)
console.log(process.memoryUsage().heapUsed)
}
p.then(() => { // instead if we do p = p.then(() => it will work correctly
return Promise.resolve().delay(5)
.then(function() {
actual++
})
});
})
.on('end', () => {
p.then(() => { // instead if we do p = p.then(() => it will work correctly
console.log(actual); // 4999977 or something like this
console.log(count); // 5000000
});
});
stream.pipe(csvStream);
答案 0 :(得分:1)
如果您延迟增加actual
计数,但从不等待承诺(抛出then
的结果),则流可能会结束,并非所有增量都已发生。在您的示例中,23个回调仍在等待5ms延迟。顺便说一句,将所有这些链接在同一个p = Promise.resolve()
上并没有多大意义,您可以立即执行所有操作。
如果你正在做p = p.then(…)
,那么你建立了一个很长的承诺链。这不应该泄漏任何内存,但会使用大量内存 - 所有5ms延迟按顺序链接在一起,并且您的脚本将运行(至少)25000秒。在开始时读入文件,生成数百万个promise,然后它们一个接一个地解析(并且可以被垃圾收集)。
要按顺序执行此顺序方法,您可能应该使用流的背压系统。
但你也可以同时等待延迟,一次没有太多活着的承诺:
p = Promise.all([p, Promise.delay(5)]).then(() => {
actual++;
});
答案 1 :(得分:1)
嗯,你希望承诺并行运行,所以不能链接它们。
allp = [];
....
.on('data', (row) => {
...
allp.push( p.then(() => {...}) );
}
...
.on('end', () => {
Promise.all(allp).then(() => {})
当然,您正在为每个活动创建一个Promise。
如果你需要在结束前解除承诺,那么你需要自己做。
既然您似乎对承诺的返回值不感兴趣,但只是在副作用(增加计数)中,您可以这样做
.on('data', (row) => {
...
if (allp.length > 50) allp = [Promise.all(allp).then(()=>null)];
allp.push( p.then(() => {...}) );
}
这样,50个承诺将被分组,一旦他们解决,他们将被一个承诺(将进入下一个50 ......)取代。
.then(()=>null)
确保来自Promise.all的结果数组也被丢弃。 (相反,对于null的一个承诺将在allp中)
这取决于Promise.all的实现。如果Promise.all在解析时释放了每个promise(并且结果可用),那么这是完美的。
如果Promise.all等待所有50个承诺,然后全部释放它们,那么这仍然有效,除非每个50人组有一个非常长的运行承诺。
您可以使用延期承诺的反模式。
在开始时创建一个延期承诺。
var resolve;
var asyncRunningCount = 1; // start with 1
var p2 = new Promise(function() {
resolve = arguments[0];
});
在开启数据
.on('data', (row) => {
...
asyncRunningCount++;
p.then(() => {work}) )
.then(() {
asyncRunningCount--;
if (asyncRunningCount == 0) resolve(); // no more tasks running
} );
}
.on('end', () => {
asyncRunningCount--;
// remove the 1 that was set on start. No more new tasks will be added
if (asyncRunningCount == 0) resolve(); // no more tasks running
p2.then(() => { all done })
如果正在运行的任务计数暂时降至0,则启动时的值1会阻止p2被解析。
在开(结束)时,1递减。如果所有任务都完成,则asyncRunningCount将为0.这可以通过on(结束)的减量或on(数据)的减量来实现。
p2.then,将在所有任务完成后运行。
所有其他承诺在完成后将被释放。 实际上在(数据)中你不需要承诺。只需启动异步任务,当异步任务完成后,减少asyncRunningCount,并检查0。
这仍然意味着如果数据进入非常快,许多承诺会并行开始。 但是如果你没有启动承诺,那么你需要存储传入的数据,因此将以任何一种方式使用内存。