Promise.all 是并行进行的?但是放到了microtask queue里?all的并行指的是什么?
发布于 8 年前 作者 AberGLJ 12140 次浏览 来自 问答

关于promise.all详细原理

19 回复

在我看来Promise.all只是一个语法糖,和并行没什么关系吧

function all(parr){
    var maxL = parr.length;
    return new Promise(function(ok,no){
        var _value = [],pass=0;
        var _then = function(i){
            return function(data){
                pass++;
                _value[i] = data;
                if(pass == maxL){
                    ok(_value);
                }
            }
        }
        if(parr.length <= 0) return ok([]);
        for(var i=0; i<parr.length; i++){
            Promise.resolve(parr[i]).then(_then(i),no)
        }
        
    })
}

@cnwhy thanks,感觉上promise.all并不是并行的,但是看到很多人都说是。而我虽然没看他的实现源码,但是感觉他是放在microtask queue里面去执行的。也就是说应该是串行。所以想看看有没了解它的原理的来解释下。

Promise.all 接收一个 promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用 .then 方法。

function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open(‘GET’, URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL(‘http://azu.github.io/promises-book/json/comment.json’).then(JSON.parse); }, people: function getPeople() { return getURL(‘http://azu.github.io/promises-book/json/people.json’).then(JSON.parse); } }; function main() { return Promise.all([request.comment(), request.people()]); } // 运行示例 main().then(function (value) { console.log(value); }).catch(function(error){ console.log(error); });

可以运行这个demo看看

@lpm0205 thank.但是我需要的是它原理,很多人说他是并行执行的,但是结果上并没有说明是并行执行的。

确实是并行的,翻了下源码大致流程应该是这样的(node v6.9.1):

function PromiseAll(iterable) {
	var deferred = NewPromiseCapability(this);
	var resolutions = new InternalArray();
	function CreateResolveElementFunction(index, values, promiseCapability) {
    	var alreadyCalled = false;
    	return (x) => {
        	if (alreadyCalled === true) return;
        	alreadyCalled = true;
			//把这个promise实例的执行结果缓存到resolutions数组中
        	values[index] = x;
    	};
	}

	var i = 0;
	for (var value of iterable) {
		//把传入的参数用promise.resolve转化为Promise对象实例
    	var nextPromise = this.resolve(value);
		//转换完成后调用then方法执行
    	nextPromise.then(
        CreateResolveElementFunction(i, resolutions, deferred),
        deferred.reject);
    	++i;
	}
	return deferred.promise;
}

这里看起来就是把Promise.all传入的数组,用for of进行了遍历,然后对每一项先用promise.resolve(this.resolve)转化为Promise对象并且赋值给nextPromise,接着就调用nextPromise.then方法,把每一个promise对象的结果缓存到resolutions数组中:

values[index] = x

最后返回deferred.promise,当我们调用Promise.all([]).then的时候,得到的就是一个数组,里面的每一项元素对应传入的每一项Promise的处理结果。如果理解有误,欢迎指正

至于提到的并行,应该说绝大部分时候我们指的是异步操作的并行 实际上来说:

Promise.all([异步操作1,异步操作2,异步操作3]).then

虽然依旧是循环先执行异步操作1.then,再执行异步操作2.then,但是这个for of循环并不会等到异步操作.then执行完成后再去执行异步操作2.then,上面的代码很容易看到决定resolutions数组结果顺序的变量i是传入CreateResolveElementFunction函数的,因此这个for of循环体现出了这些异步操作的并行

@hyj1991 这个好像我也看过,但是这是promise,通常来讲整体代码是放到同一个macrotask 跟microtask里面的,而promise是会放到microtask queue里面执行,这样的话,其实它并不是并行,而是串行。因为丢到queue里面是先进先出的(如无其他特殊处理的话)。

@hyj1991 所以其实他并不是真正意义上的并行,比如说在内存上并行执行。我只是比较疑惑说他是并行但是确不是并行的一个结果。thanks.

  • 田径赛跑,所有的人都跑完了,all就结束了。
  • 田径赛跑,第一个跑到了,race就结束了。

@AberGLJ microtask会一次性把当前队列任务全部执行掉,和Promise.all的串行无关吧:

while (pending_microtask_count() > 0) {
	int num_tasks = pending_microtask_count();
	Handle<FixedArray> queue(heap()->microtask_queue(), this);
	//清空microTask队列
	set_pending_microtask_count(0);
	heap()->set_microtask_queue(heap()->empty_fixed_array());
	FOR_WITH_HANDLE_SCOPE(isolate, int, i = 0, i, i < num_tasks, i++, {
		Handle<Object> microtask(queue->get(i), this);
		//...处理代码
	});
}

而且在Node中,microTask仅在每一次_tickCallback的调用中被执行:

  • 把存储的nextTick函数的nextTickQueue中全部拿出来处理掉(可能nextTickQueue没有待处理函数)
  • 然后才会执行_runMicrotasks操作;

而_tickCallback本身看起来目前只有三处会被调用:runMain之后(入口js文件解析完毕,进入event loop之前),以及io回调和timer回调。

所以我倾向于每一次的microtask执行的是上面源码中的PromiseAll函数本身,而不是把promise.all注册的promise实例分散注册到microtask_queue里面去

@i5ting thank. 我现在理解的promise.all应该是数组里面的promise其实都是放到queue里面的,所以它的并行意思指的for of循环体现出了这些异步操作的并行,并非真正的并行。

@AberGLJ for of控制的执行,必然不是真正的类似多线程实现的并行,这一点我觉得是毋庸置疑的

@hyj1991 所以你的意思是PromiseAll函数是整体放到了microtask里面去执行,而不是在macrotask 或者叫task里面执行的?然后他应该什么时候执行里面的promise实例呢?

@hyj1991 对,我也认为并非真正的类似多线程实现的并行。所以才对它感到困惑。

@AberGLJ 回来又翻了下代码,之前应该是想错了,Promise链的每一环的执行其实在new Promise的时候就执行掉了:

var GlobalPromise = function Promise(resolver) {
	try {
    	//传入的(resolve, reject)=>{}处理函数
    	resolver(callbacks.resolve, callbacks.reject);
	} catch (e) {
		%_Call(callbacks.reject, UNDEFINED, e);
	} finally {
		%DebugPopPromise();
	}
	return promise;
};

所以实际上调用Promise.all时,传入的数组里面如果都是Promise实例的话,其实这里已经按照new Promise的顺序执行了,并且会记录下当前的状态(-1,0,+1)。

至于注册到microtaskqueue中执行的部分,并不是我之前以为的promise构造方法中的处理函数,而实际上是Promise的回调函数,即then方法里注册的onResolve和onReject(如果有的话)。

这样一来就比较明白了,new Promise中的操作不管是异步还是同步,都是按照在代码中new的顺序串行执行的;然后then方法注册的回调会分两种情况(准确说是三种,但是resolve和reject状态都会进microtask queue所以认为是一种):

  • 如果当前的promise实例状态是0(Pending,比如异步获取数据未返回),那么把then方法传入的onResolve和onReject方法塞进promise实例初始化时的一个内建数组中(new InternalArray),名字对应是resolve方法塞入promiseOnResolveSymbol数组,reject方法塞入promiseOnRejectSymbol数组
  • 如果当前的promise实例状态为1或者-1(resolve或者reject),那么直接会调用PromiseEnqueue注册一个回调函数到microtask queue,这个回调函数的作用就是执行对应的onResolve或者obReject方法

那么第一种的pending状态是调用then方法时该promise实例的状态,等我们写的异步方法执行结束后会转向对应的resolve或者reject状态,此时(既当我们resolve(data)或者reject(error)时),会调用真正的PromiseDone方法,该方法功能就是判断上面的promiseOnResolveSymbol数组或者promiseOnRejectSymbol数组中是否有待处理的任务,也就是上面第一种情况下会添加的then函数传入的方法,如果有,那么同样会调用PromiseEnqueue注册一个回调函数到microtask queue来处理回调

@AberGLJ 好好理解一下事件驱动吧

promise.all 本身并不是并行的; 并行都是,调用的那些回调函数,是异步属性;

它相当于等待所有事件回调 执行完成 再执行; 与EventProxy这个组件的 原理很相近。 都要继承自EventEmitter

@slclub ES6 v8内建的Promise和EventEmitter没有关系,PromiseAll函数是靠计数变量–count来控制结果的:

if (--count === 0) {
    var valuesArray = [];
	//内建数组赋值函数,最终传递给Promise.all().then的数据就是valuesArray
    %MoveArrayContents(values, valuesArray);
	//类似于f.call的函数调用方法,把结果传给deferred对象
    %_Call(promiseCapability.resolve, UNDEFINED, valuesArray);
 }

@hyj1991 恩恩,其实new promise的时候是在task队列中执行(整体的代码),所以在执行的时候就已经是按照在代码中new的顺序将promise注册到microtaskqueue里面,其实就是把then里面的函数放到了队列里去执行。所以本身promise.all并非真正的并行执行,实际还是根据注册顺序在队列里串行执行的。

回到顶部