使用 async 发起多个耗时任务,超过规定时间后,能否取消正在进行的任务,进入结果处理回调。
发布于 7 年前 作者 W-v-W 4876 次浏览 来自 问答

代码如下 :

async.mapLimit(items, 10, function(item, callback){
	taskStart(item, function(err, data){	// 发起耗时任务
		if(err){
			callback();								
		}else{
			callback(null, data);
		}
	}); 	
}, function(err, resul){
	result = result.filter(n => n);
	// ...
});

**问题 : ** 有没有简单的办法,设置任务时间上限,超时后直接进入结果处理。

7 回复

本楼正解

Promise.race() Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。

var p = Promise.race([p1, p2, p3]); 上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将Promise的状态变为reject,否则变为resolve。

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));

上面代码中,如果5秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

可以, 使用 promise.timeout https://cnodejs.org/topic/574f88811176fe255bb8d7e3

支持资源清理, 像发起一个 http 请求, 在 onCancel 里写上 req.abort 之后, 如果不手动 abort, 请求还是会继续 本质上就是 Promise.race, 但是多了资源清理

下载文件示例见 https://github.com/magicdawn/yun-playlist-downloader/blob/v0.11.0/lib/index.js#L132-L135 fn(arg1, arg2, onCancel) -> Promise 使用 fn_withtimeout = ptimeout(fn, timeout, true) 最后一个 true 标识会在 fn 执行的时候传一个 onCancel

@zengming00 合成的 Promise 如何设置并发数?

@zengming00 学习了 mark~,之前一直不知道promise.race 干毛用 = =

@zengming00 不知道你的“本楼正解”是自己写的还是楼主改的,感觉你的思路与楼主的示例代码有些不符。楼主示例代码给人的感觉是要运行一系列任务,如果所有都在规定时间内完成了,结果是这一系列任务结果的一个集合;如果其中有任务超时了,到达查实时间后不会卡住也会返回结果,结果为其中已经执行好的任务结果的一个集合。而你给出的 Promise.race 的解决方案,只要有一个任务成功执行就会 resolve,这样结果中不会包含其他任务的执行结果;在所有任务都执行超时时才会触发附加的超时的 reject。


以下是我的一个思路,如果理解不对也请谅解。

根据自己最冲动最初始的想法写代码,可以满足要求,但是略显繁杂,在所有需要 callback 的地方都需要判断。

'use strict';

const async = require('async');

const list = [1, 2, 3, 4, 5, 6];

async.mapLimit(list, 10, function (item, callback) {
    let alreadyCallback = false;
    setTimeout(function () {
        if (!alreadyCallback) {
            callback(new Error('manual set error'));
            return alreadyCallback = true;
        } else {
            // do nothing
        }
    }, 3000);

    // // 超时任务
    // if (item === 3) {
    //     setTimeout(function () {
    //         if (!alreadyCallback) {
    //             callback(null, item + 200);
    //             return alreadyCallback = true;
    //         } else {
    //             // do nothing
    //         }
    //     }, 5000);
    // } else {
        if (!alreadyCallback) {
            callback(null, item + 100);
            return alreadyCallback = true;
        } else {
            // do nothing
        }
    // }
}, function (error, results) {
    if (error) {
        return console.log(error);
    }

    console.log(results);
});

程序员的常规思路,当遇到需要重复的繁杂代码时一定要想办法精简。

'use strict';

const async = require('async');

const list = [1, 2, 3, 4, 5, 6];

async.mapLimit(list, 10, function (item, callback) {
    let alreadyCallback = false;
    const _callback = function (error, result) {
        if(!alreadyCallback) {
            callback(error, result);
            alreadyCallback = true
        }
    };

    setTimeout(function () {
        return _callback(new Error('manual set error'));
    }, 3000);

    // // 超时任务
    // if (item === 3) {
    //     setTimeout(function () {
    //        return _callback(null, item + 200);
    //     }, 5000);
    // } else {
        return _callback(null, item + 100);
    // }
}, function (error, results) {
    if (error) {
        return console.log(error);
    }

    console.log(results);
});

至于在有任务超时时是触发错误,还是忽略错误,返回一个标识的结果到结果集合中,然后再从结果集合中过滤掉超时标识,这个倒是可以根据需求自己修改代码了。

@DuanPengfei 先感谢@DuanPengfei 的启发。 我尝试过:

  1. 超时时主动调用 callback().
  2. 进行包装 var wrapped = async.timeout(foo, 3000); 然后使用 async.mapLimit(items, 10, wrapped ...);

但是,两种方法,会造成重复调用 callback() ,会抛出错误 if (fn === null) throw new Error("Callback was already called."); 方法1 可以通过状态标记 alreadyCallback 避免 Error: Callback was already called.. 方法2 即使避免重复调用 callback(),还是存在计时的 bug, 并发数小于任务总数时,单个任务处理时间不超时,并不能保证整体处理时间不超时。


最终实现代码


var callbacks = [];

var timer = setTimeout(function(){
    callbacks.some(function(obj){
        if(!obj.hasCalled){
            obj.fn(new Error('TIME-OUT'));
            obj.hasCalled = true;
            return true;
        }
        return false;
    });
}, 10000);

var foo = function(item, callback){
    var obj = {'hasCalled':false, 'fn':callback};
    callbacks.push(obj);
    var _callback = function(err, result){
        if(!obj.hasCalled){
            callback(err, result);
            obj.hasCalled = true;
        }
    }
    taskStart(item, function(err, data){	// 发起耗时任务
        if(err){
            _callback();					
        }else{
            _callback(null, data);
        }
    }); 	
};
	
async.mapLimit(items, 10, foo, function(err, resul){
    clearTimeout(timer);
    if(err){
        console.log(err.message);
    }
    result = result.filter(n => n);
    // ...
});
	

@W-v-W 恩恩,我没有考虑到每次并发耗时累计起来的总时间问题,最终代码这样的实现就能达到需求了 😊

回到顶部