用async并发执行几个函数,同时写入一个全局的变量,是否保证有序?
发布于 9 年前 作者 crystaldust 8069 次浏览 最后一次编辑是 8 年前 来自 问答

事情是这样的,产品的同事希望在一个用户列表中,按照不同的标准选取几类用户(假设有A, Bl两种用户),然后同时呈现。但是有些用户可能同时满足2个标准,比如某个用户既符合A标准,也符合B标准,这样返回的结果如果不加限定,就会出现同一个用户出现多次的情况。

现在我是这么考虑的,先去找A类的用户,查找结束后,把A类用户的_id传递给第二个函数,第二个函数在查找B类用户时候,把第所有A类用户的_id都剔除掉。程序结构大概是这样:



var exclude = [];

async.series( [
	function( callback ) {
		db.collection('users').find( { /*category A的条件*/} ).toArray( function( err, users ) {
			users.forEach( function( user ) {
				exclude.push( user._id );
			} );
			callback( null, users );
		} )
	},

	function( callback ) {
		db.collection('users').find( { /*category B的条件*/ _id : { $not : { $in : exclude } }/*限定不包含category A的用户*/ } ).toArray( function( err, users ) {
			callback( null, users );
		} )
	}

], function( err, result ) {
	var users_A = result[0];
	var users_B = result[1];
	// Handle category A and B...	
} )

为了保证不重复,需要A执行完后在执行B。顺序执行,响应时间太长。如果改成并发执行,那么每个函数去查找用户时,都要包含{$not : { $in : exclude } }条件。关键是是否会出现这样一种情况: 第一个函数先执行回调,然后向exclude写入id,还没有写完时,第二个函数执行回调了,但是exclude还是空的,那么exclude的限定在第二个函数里其实就没有作用了。

小弟表达能力较差,说的不清楚的地方,还请大家指出,期待各路高手给点儿意见啊,跪谢先 m( _ _ ) m

16 回复

@i5ting 大哥你好,你是说改成并发的,可以避免出现我说的那种情况吗?

试试

var async = require('async');

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
        console.log('1');
    },
    function(arg1, arg2, callback){
        callback(null, 'three');
        console.log(arg1);
        console.log(arg2);
    },
    function(arg1, callback){
        // arg1 now equals 'three'
        callback(null, 'done');
        console.log(arg1);
    }
], function (err, result) {
    console.log(result);
    // result now equals 'done'
  //  console.log('4');
});

非要并行的话,你把查到userid作为一个key放到一个对象中(数据不多的话)?用hasOwnProperty来判断?

@luoyjx 嗯,不管是否并行,都需要把本次找到的结果放到一个全局的对象(这里是用数组)中,然后其他的函数需要用到这个全局的对象

不用这么麻烦,javascript 是单线程的,不用考虑并发冲突。给一个全局对角 var all = {}; 然后并发执行,每个FUNC里面取得users一个个 all[user.id] = user ,这样就O了。 根据 javascript 的定义,同一时间,只有同一段代码在执行(如一个function体)。 那么你并进操作是没有问题的,同一时间只可能会有一个操作对 all 进行操作。 that’s the beauty part of JS. 刚好刚才在首页有看到这条链接, 你可以参考一下, 有详细的说明js执行原理。http://blog.getify.com/promises-part-1/

@klesh That’s cool! 太感谢了,我仔细看一下。另外大哥你是用五笔打字的吧?厉害啊

噢,对象打成对角了。。。五笔在我那年代是学电脑的必修课。不像现在。再者,我拼音太差了。h,g啥的太复杂。这种执行方式 python 里面叫 coroutine (python 的机制没 nodjs 这么彻底)。完全避免了线程和进程切换的开销,未来必须大火!

@klesh 哇塞,五笔是必修课啊,太酷了!

很多人不是号称不需要理解异步,只要会写回调就会异步了么?

回调执行的时候就是同步的,异步跟你已经没关系了。 回调说白了就是异步完成后的函数调用,跟普通函数没有区别,就是把异步结果传了进来,回调山怎么来的?就是为了保证回调按照我们制定的顺序去执行,如果你对顺序没什么要求,回调山其实不是必须的,没有回调山,就是所谓并行,但是这个并行的结果还是同步执行的。回调从来都是一个接一个运行的,是单线程的,所以回调之外的公共变量在回调内都是可以无锁访问的。async的原理就是利用这个。

简单的说,回调本身是同步执行的,回调山是为了保证回调执行的顺序,回调是异步的结果,异步的事情是其他代码做的,跟回调没有关系。最后我再宣扬下,回调不是必须的,为了更好的异步编程,回调必须死。那些心里不爽的自己去看javascript语言进化的路径,看看是不是逐步的在去回调,利用回调来保证异步执行的顺序编程体验太差了,不利于程序员组织复杂的业务代码。形式同步必然是将来的发展方向,我今天在这里的预言,大家三五年之后可以回过头来看,时间会证明一切。

@crystaldust 顺序的正确,并行不可行,会出现你说的情况,因为你不知道哪个回调会被先执行,如果B先好,exclude就是空的。

一定要理解异步是别人的代码的异步,跟自己的代码是无关的,你自己的代码本质上是同步的,你能够左右的只有执行顺序,但是他们不是并行的,从这个角度来说,在保证顺序的情况下,你的代码可以访问exclude,你写的所有代码都只有执行的先后关系,而没有任何并行异步的关系。回调山是为了保证执行顺序,回调执行那一刻起,异步代码已经跟我们没关系了。如果没有回调山保证顺序,因为异步代码不是我们能够控制的,他们的执行顺序我们不能知晓,无法得到保证,所以我们自然无法知道谁先好,谁后好,顺序是未知的。

你的代码如果换成并行的,就会出现有的时候正常排除,有的时候又不正常的情况,顺序无法确定。

你能够用exclude是因为你的代码都是在单线程条件下同步执行的,你需要通过回调山来保证顺序是因为别人异步代码的返回顺序你无法保证。我觉得你已经比较深刻的理解了这个问题,你只是不太确定,相信自己的直觉。

再次反回调,楼主的问题充分说明了回调恶心之处,形式同步必将取代回调。

@coordcn callback hell not callback hill

@20082496 我从来没觉得回调是地狱,回调只是很恶心,但从来都是不是地狱,不要人云亦云,如果认为回调是地狱的,那多半连异步的门还没入,如果回调是地狱,node根本没有存在的必要。

回调是能够清晰表达异步过程的,但是前提是彻底的理解了异步。没有这个基础,回调当然就成了一些人眼中的地狱了。回调虽然可以表达异步过程,但是有天生的缺陷,回调如果不进行适当处理,具有很强的传染性,回调对错误处理也不那么直观,层层回调也使程序编写困难,在一些复杂逻辑下,回调山是不可避免的。

地狱进去了就出不来,山只要有足够的毅力,还是能翻过去的。es6,7有了一些改变,promise,generator/yield,await/async都在逐步解决这个问题,回调被替代只是时间问题,但异步将会越来越普及。

将来的异步代码应该是这样的:

var data,err = fs.readFile('test.txt');
if(err){
	console.log(err.message);
}
console.log(data);

所有跟形式相关的异步都不是必要的,将来的模式是去除所有异步相关的形式,不需要任何关键字的,隐式异步,也即形式同步。

@coordcn 看的一知半解,但是还是要赞一下。

@coordcn 很有见解啊,赞

@coordcn 我刚开始接触Node.JS那会,遇到回调山非常痛苦,经常抱怨,后来随着对Nodejs理解的加深,又接触了EventProxy,async等框架,慢慢的也就习惯了。

回到顶部