Generator并发
正如我们在第一章和本章早先讨论过的,另个同时运行的“进程”可以协作地穿插它们的操作,而且许多时候这可以产生非常强大的异步表达式。
坦白地说,我们前面关于多个generator并发穿插的例子,展示了这真的容易让人糊涂。但我们也受到了启发,有些地方这种能力十分有用。
回想我们在第一章中看过的场景,两个不同但同时的Ajax应答处理需要互相协调,来确保数据通信不是竟合状态。我们这样把应答分别放在res
数组的不同位置中:
function response(data) {
if (data.url == "http://some.url.1") {
res[0] = data;
}
else if (data.url == "http://some.url.2") {
res[1] = data;
}
}
但是我们如何在这种场景下使用多generator呢?
// `request(..)` 是一个基于Promise的Ajax工具
var res = [];
function *reqData(url) {
res.push(
yield request( url )
);
}
注意: 我们将在这里使用两个*reqData(..)
generator的实例,但是这和分别使用两个不同generator的一个实例没有区别;这两种方式在道理上完全一样的。我们过一会儿就会看到两个generator的协调操作。
与不得不将res[0]
和res[1]
赋值手动排序不同,我们将使用协调过的顺序,让res.push(..)
以可预见的顺序恰当地将值放在预期的位置。如此被表达的逻辑会让人感觉更干净。
但是我们将如何实际安排这种互动呢?首先,让我们手动实现它:
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it1.next().value;
var p2 = it2.next().value;
p1
.then( function(data){
it1.next( data );
return p2;
} )
.then( function(data){
it2.next( data );
} );
*reqData(..)
的两个实例都开始发起它们的Ajax请求,然后用yield
暂停。之后我们再p1
解析时继续运行第一个实例,而后来的p2
的解析将会重启第二个实例。以这种方式,我们使用Promise的安排来确保res[0]
将持有第一个应答,而res[1]
持有第二个应答。
但坦白地说,这是可怕的手动,而且它没有真正让generator组织它们自己,而那才是真正的力量。让我们用不同的方法试一下:
// `request(..)` 是一个基于Promise的Ajax工具
var res = [];
function *reqData(url) {
var data = yield request( url );
// 传递控制权
yield;
res.push( data );
}
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it1.next().value;
var p2 = it2.next().value;
p1.then( function(data){
it1.next( data );
} );
p2.then( function(data){
it2.next( data );
} );
Promise.all( [p1,p2] )
.then( function(){
it1.next();
it2.next();
} );
好的,这看起来好些了(虽然仍然是手动),因为现在两个*reqData(..)
的实例真正地并发运行了,而且(至少是在第一部分)是独立的。
在前一个代码段中,第二个实例在第一个实例完全完成之前没有给出它的数据。但是这里,只要它们的应答一返回这两个实例就立即分别收到他们的数据,然后每个实例调用另一个yield
来传送控制。最后我们在Promise.all([ .. ])
的处理器中选择用什么样的顺序继续它们。
可能不太明显的是,这种方式因其对称性启发了一种可复用工具的简单形式。让我们想象使用一个称为runAll(..)
的工具:
// `request(..)` 是一个基于Promise的Ajax工具
var res = [];
runAll(
function*(){
var p1 = request( "http://some.url.1" );
// 传递控制权
yield;
res.push( yield p1 );
},
function*(){
var p2 = request( "http://some.url.2" );
// 传递控制权
yield;
res.push( yield p2 );
}
);
注意: 我们没有包含runAll(..)
的实现代码,不仅因为它长得无法行文,也因为它是一个我们已经在先前的 run(..)
中实现的逻辑的扩展。所以,作为留给读者的一个很好的补充性练习,请你自己动手改进run(..)
的代码,来使它像想象中的runAll(..)
那样工作。另外,我的 asynquence 库提供了一个前面提到过的runner(..)
工具,它内建了这种能力,我们将在本书的附录A中讨论它。
这是runAll(..)
内部的处理将如何操作:
- 第一个generator得到一个代表从
"http://some.url.1"
来的Ajax应答,然后将控制权yield
回到runAll(..)
工具。 - 第二个generator运行,并对
"http://some.url.2"
做相同的事,将控制权yield
回到runAll(..)
工具。 - 第一个generator继续,然后
yield
出他的promisep1
。在这种情况下runAll(..)
工具和我们前面的run(..)
做同样的事,它等待promise解析,然后继续这同一个generator(没有控制传递!)。当p1
解析时,runAll(..)
使用解析值再一次继续第一个generator,而后res[0]
得到它的值。在第一个generator完成之后,有一个隐式的控制权传递。 - 第二个generator继续,
yield
出它的promisep2
,并等待它的解析。一旦p2
解析,runAll(..)
使用这个解析值继续第二个generator,于是res[1]
被设置。
在这个例子中,我们使用了一个称为res
的外部变量来保存两个不同的Ajax应答的结果——这是我们的并发协调。
但是这样做可能十分有帮助:进一步扩展runAll(..)
使它为多个generator实例提供 分享的 内部的变量作用域,比如一个我们将在下面称为data
的空对象。另外,它可以接收被yield
的非Promise值,并把它们交给下一个generator。
考虑这段代码:
// `request(..)` 是一个基于Promise的Ajax工具
runAll(
function*(data){
data.res = [];
// 传递控制权(并传递消息)
var url1 = yield "http://some.url.2";
var p1 = request( url1 ); // "http://some.url.1"
// 传递控制权
yield;
data.res.push( yield p1 );
},
function*(data){
// 传递控制权(并传递消息)
var url2 = yield "http://some.url.1";
var p2 = request( url2 ); // "http://some.url.2"
// 传递控制权
yield;
data.res.push( yield p2 );
}
);
在这个公式中,两个generator不仅协调控制传递,实际上还互相通信:通过data.res
,和交换url1
与url2
的值的yield
消息。这强大到不可思议!
这样的认识也是一种更为精巧的称为CSP(Communicating Sequential Processes——通信顺序处理)的异步技术的概念基础,我们将在本书的附录B中讨论它。