Generator并发

正如我们在第一章和本章早先讨论过的,另个同时运行的“进程”可以协作地穿插它们的操作,而且许多时候这可以产生非常强大的异步表达式。

坦白地说,我们前面关于多个generator并发穿插的例子,展示了这真的容易让人糊涂。但我们也受到了启发,有些地方这种能力十分有用。

回想我们在第一章中看过的场景,两个不同但同时的Ajax应答处理需要互相协调,来确保数据通信不是竟合状态。我们这样把应答分别放在res数组的不同位置中:

  1. function response(data) {
  2. if (data.url == "http://some.url.1") {
  3. res[0] = data;
  4. }
  5. else if (data.url == "http://some.url.2") {
  6. res[1] = data;
  7. }
  8. }

但是我们如何在这种场景下使用多generator呢?

  1. // `request(..)` 是一个基于Promise的Ajax工具
  2. var res = [];
  3. function *reqData(url) {
  4. res.push(
  5. yield request( url )
  6. );
  7. }

注意: 我们将在这里使用两个*reqData(..)generator的实例,但是这和分别使用两个不同generator的一个实例没有区别;这两种方式在道理上完全一样的。我们过一会儿就会看到两个generator的协调操作。

与不得不将res[0]res[1]赋值手动排序不同,我们将使用协调过的顺序,让res.push(..)以可预见的顺序恰当地将值放在预期的位置。如此被表达的逻辑会让人感觉更干净。

但是我们将如何实际安排这种互动呢?首先,让我们手动实现它:

  1. var it1 = reqData( "http://some.url.1" );
  2. var it2 = reqData( "http://some.url.2" );
  3. var p1 = it1.next().value;
  4. var p2 = it2.next().value;
  5. p1
  6. .then( function(data){
  7. it1.next( data );
  8. return p2;
  9. } )
  10. .then( function(data){
  11. it2.next( data );
  12. } );

*reqData(..)的两个实例都开始发起它们的Ajax请求,然后用yield暂停。之后我们再p1解析时继续运行第一个实例,而后来的p2的解析将会重启第二个实例。以这种方式,我们使用Promise的安排来确保res[0]将持有第一个应答,而res[1]持有第二个应答。

但坦白地说,这是可怕的手动,而且它没有真正让generator组织它们自己,而那才是真正的力量。让我们用不同的方法试一下:

  1. // `request(..)` 是一个基于Promise的Ajax工具
  2. var res = [];
  3. function *reqData(url) {
  4. var data = yield request( url );
  5. // 传递控制权
  6. yield;
  7. res.push( data );
  8. }
  9. var it1 = reqData( "http://some.url.1" );
  10. var it2 = reqData( "http://some.url.2" );
  11. var p1 = it1.next().value;
  12. var p2 = it2.next().value;
  13. p1.then( function(data){
  14. it1.next( data );
  15. } );
  16. p2.then( function(data){
  17. it2.next( data );
  18. } );
  19. Promise.all( [p1,p2] )
  20. .then( function(){
  21. it1.next();
  22. it2.next();
  23. } );

好的,这看起来好些了(虽然仍然是手动),因为现在两个*reqData(..)的实例真正地并发运行了,而且(至少是在第一部分)是独立的。

在前一个代码段中,第二个实例在第一个实例完全完成之前没有给出它的数据。但是这里,只要它们的应答一返回这两个实例就立即分别收到他们的数据,然后每个实例调用另一个yield来传送控制。最后我们在Promise.all([ .. ])的处理器中选择用什么样的顺序继续它们。

可能不太明显的是,这种方式因其对称性启发了一种可复用工具的简单形式。让我们想象使用一个称为runAll(..)的工具:

  1. // `request(..)` 是一个基于Promise的Ajax工具
  2. var res = [];
  3. runAll(
  4. function*(){
  5. var p1 = request( "http://some.url.1" );
  6. // 传递控制权
  7. yield;
  8. res.push( yield p1 );
  9. },
  10. function*(){
  11. var p2 = request( "http://some.url.2" );
  12. // 传递控制权
  13. yield;
  14. res.push( yield p2 );
  15. }
  16. );

注意: 我们没有包含runAll(..)的实现代码,不仅因为它长得无法行文,也因为它是一个我们已经在先前的 run(..)中实现的逻辑的扩展。所以,作为留给读者的一个很好的补充性练习,请你自己动手改进run(..)的代码,来使它像想象中的runAll(..)那样工作。另外,我的 asynquence 库提供了一个前面提到过的runner(..)工具,它内建了这种能力,我们将在本书的附录A中讨论它。

这是runAll(..)内部的处理将如何操作:

  1. 第一个generator得到一个代表从"http://some.url.1"来的Ajax应答,然后将控制权yield回到runAll(..)工具。
  2. 第二个generator运行,并对"http://some.url.2"做相同的事,将控制权yield回到runAll(..)工具。
  3. 第一个generator继续,然后yield出他的promisep1。在这种情况下runAll(..)工具和我们前面的run(..)做同样的事,它等待promise解析,然后继续这同一个generator(没有控制传递!)。当p1解析时,runAll(..)使用解析值再一次继续第一个generator,而后res[0]得到它的值。在第一个generator完成之后,有一个隐式的控制权传递。
  4. 第二个generator继续,yield出它的promisep2,并等待它的解析。一旦p2解析,runAll(..)使用这个解析值继续第二个generator,于是res[1]被设置。

在这个例子中,我们使用了一个称为res的外部变量来保存两个不同的Ajax应答的结果——这是我们的并发协调。

但是这样做可能十分有帮助:进一步扩展runAll(..)使它为多个generator实例提供 分享的 内部的变量作用域,比如一个我们将在下面称为data的空对象。另外,它可以接收被yield的非Promise值,并把它们交给下一个generator。

考虑这段代码:

  1. // `request(..)` 是一个基于Promise的Ajax工具
  2. runAll(
  3. function*(data){
  4. data.res = [];
  5. // 传递控制权(并传递消息)
  6. var url1 = yield "http://some.url.2";
  7. var p1 = request( url1 ); // "http://some.url.1"
  8. // 传递控制权
  9. yield;
  10. data.res.push( yield p1 );
  11. },
  12. function*(data){
  13. // 传递控制权(并传递消息)
  14. var url2 = yield "http://some.url.1";
  15. var p2 = request( url2 ); // "http://some.url.2"
  16. // 传递控制权
  17. yield;
  18. data.res.push( yield p2 );
  19. }
  20. );

在这个公式中,两个generator不仅协调控制传递,实际上还互相通信:通过data.res,和交换url1url2的值的yield消息。这强大到不可思议!

这样的认识也是一种更为精巧的称为CSP(Communicating Sequential Processes——通信顺序处理)的异步技术的概念基础,我们将在本书的附录B中讨论它。