4.6 生成器并发
就像我们在第 1 章和本章前面都讨论过的一样,两个同时运行的进程可以合作式地交替运作,而很多时候这可以产生 (双关,原文为 yield:既指产生又指 yield 关键字)非常强大的异步表示。
坦白地说,本部分前面的多个生成器并发交替执行的例子已经展示了如何使其看起来令人迷惑。但是,我们已经暗示过了,在一些场景中这个功能会很有用武之地的。
回想一下第 1 章给出的一个场景:其中两个不同并发 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; } }
但是这种场景下如何使用多个并发生成器呢?
// request(..)是一个支持Promise的Ajax工具 var res = []; function *reqData(url) { res.push( yield request( url ) ); }
这里我们将使用生成器 *reqData(..) 的两个实例,但运行两个不同生成器的实例也没有任何区别。两种方法的过程几乎一样。稍后将会介绍两个不同生成器的彼此协调。
这里不需要手工为 res[0] 和 res[1] 赋值排 序,而是使用合作式的排 序,使得 res.push(..) 把值按照预期以可预测的顺序正确安置。这样,表达的逻辑给人感觉应该更清晰一点。
但是,实践中我们如何安排这些交互呢?首先,使用 Promise 手工实现:
var it1 = reqData( "http://some.url.1" ); var it2 = reqData( "http://some.url.2" ); var p1 = it1.next(); var p2 = it2.next(); 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] 中会放置第二个响应。
但是,坦白地说,这种方式的手工程度非常高,并且它也不能真正地让生成器自己来协调,而那才是真正的威力所在。让我们换一种方法试试:
// 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 = it.next(); var p2 = it.next(); 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(..) 内部运行的过程。
(1) 第一个生成器从第一个来自于 "http://some.url.1" 的 Ajax 响应得到一个 promise,然后把控制 yield 回 runAll(..) 工具。
(2) 第二个生成器运 行,对于 "http://some.url.2" 实现同样的操 作,把控制 yield 回 runAll(..) 工具。
(3) 第一个生成器恢复运行,通过 yield 传出其 promise p1 。在这种情况下,runAll(..) 工具所做的和我们之前的 run(..) 一样,因为它会等待这个 promise 决议,然后恢复同一个生成器(没有控制转移!)。p1 决议后,runAll(..) 使用这个决议值再次恢复第一个生成器,然后 res[0] 得到了自己的值。接着,在第一个生成器完成的时候,有一个隐式的控制转移。
(4) 第二个生成器恢复运行,通过 yield 传出其 promise p2 ,并等待其决议。一旦决议,runAll(..) 就用这个值恢复第二个生成器,设置 res[1] 。
在这个例子的运行中,我们使用了一个名为 res 的外层变量来保存两个不同的 Ajax 响应结果,我们的并发协调使其成为可能。
但是,如果继续扩展 runAll(..) 来提供一个内层的变量空间,以使多个生成器实例可以共享 ,将是非常有帮助的,比如下面这个称为 data 的空对象。还有,它可以接受 yield 的非 Promise 值,并把它们传递到下一个生成器。
考虑:
// 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 ); } );
在这一方案中,实际上两个生成器不只是协调控制转移,还彼此通信,通过 data.res 和 yield 的消息来交换 url1 和 url2 的值。真是极其强大!
这样的实现也为被称作通信顺序进程 (Communicating Sequential Processes,CSP)的更高级异步技术提供了一个概念基础。对此,我们将在本部分的附录 B 中详细讨论。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论