Generator Concurrency

As we discussed in both Chapter 1 and earlier in this chapter, two simultaneously running “processes” can cooperatively interleave their operations, and many times this can yield (pun intended) very powerful asynchrony expressions.

Frankly, our earlier examples of concurrency interleaving of multiple generators showed how to make it really confusing. But we hinted that there’s places where this capability is quite useful.

Recall a scenario we looked at in Chapter 1, where two different simultaneous Ajax response handlers needed to coordinate with each other to make sure that the data communication was not a race condition. We slotted the responses into the res array like this:

  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. }

But how can we use multiple generators concurrently for this scenario?

  1. // `request(..)` is a Promise-aware Ajax utility
  2. var res = [];
  3. function *reqData(url) {
  4. res.push(
  5. yield request( url )
  6. );
  7. }

Note: We’re going to use two instances of the *reqData(..) generator here, but there’s no difference to running a single instance of two different generators; both approaches are reasoned about identically. We’ll see two different generators coordinating in just a bit.

Instead of having to manually sort out res[0] and res[1] assignments, we’ll use coordinated ordering so that res.push(..) properly slots the values in the expected and predictable order. The expressed logic thus should feel a bit cleaner.

But how will we actually orchestrate this interaction? First, let’s just do it manually, with Promises:

  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(..)‘s two instances are both started to make their Ajax requests, then paused with yield. Then we choose to resume the first instance when p1 resolves, and then p2‘s resolution will restart the second instance. In this way, we use Promise orchestration to ensure that res[0] will have the first response and res[1] will have the second response.

But frankly, this is awfully manual, and it doesn’t really let the generators orchestrate themselves, which is where the true power can lie. Let’s try it a different way:

  1. // `request(..)` is a Promise-aware Ajax utility
  2. var res = [];
  3. function *reqData(url) {
  4. var data = yield request( url );
  5. // transfer control
  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. } );

OK, this is a bit better (though still manual!), because now the two instances of *reqData(..) run truly concurrently, and (at least for the first part) independently.

In the previous snippet, the second instance was not given its data until after the first instance was totally finished. But here, both instances receive their data as soon as their respective responses come back, and then each instance does another yield for control transfer purposes. We then choose what order to resume them in the Promise.all([ .. ]) handler.

What may not be as obvious is that this approach hints at an easier form for a reusable utility, because of the symmetry. We can do even better. Let’s imagine using a utility called runAll(..):

  1. // `request(..)` is a Promise-aware Ajax utility
  2. var res = [];
  3. runAll(
  4. function*(){
  5. var p1 = request( "http://some.url.1" );
  6. // transfer control
  7. yield;
  8. res.push( yield p1 );
  9. },
  10. function*(){
  11. var p2 = request( "http://some.url.2" );
  12. // transfer control
  13. yield;
  14. res.push( yield p2 );
  15. }
  16. );

Note: We’re not including a code listing for runAll(..) as it is not only long enough to bog down the text, but is an extension of the logic we’ve already implemented in run(..) earlier. So, as a good supplementary exercise for the reader, try your hand at evolving the code from run(..) to work like the imagined runAll(..). Also, my asynquence library provides a previously mentioned runner(..) utility with this kind of capability already built in, and will be discussed in Appendix A of this book.

Here’s how the processing inside runAll(..) would operate:

  1. The first generator gets a promise for the first Ajax response from "http://some.url.1", then yields control back to the runAll(..) utility.
  2. The second generator runs and does the same for "http://some.url.2", yielding control back to the runAll(..) utility.
  3. The first generator resumes, and then yields out its promise p1. The runAll(..) utility does the same in this case as our previous run(..), in that it waits on that promise to resolve, then resumes the same generator (no control transfer!). When p1 resolves, runAll(..) resumes the first generator again with that resolution value, and then res[0] is given its value. When the first generator then finishes, that’s an implicit transfer of control.
  4. The second generator resumes, yields out its promise p2, and waits for it to resolve. Once it does, runAll(..) resumes the second generator with that value, and res[1] is set.

In this running example, we use an outer variable called res to store the results of the two different Ajax responses — that’s our concurrency coordination making that possible.

But it might be quite helpful to further extend runAll(..) to provide an inner variable space for the multiple generator instances to share, such as an empty object we’ll call data below. Also, it could take non-Promise values that are yielded and hand them off to the next generator.

Consider:

  1. // `request(..)` is a Promise-aware Ajax utility
  2. runAll(
  3. function*(data){
  4. data.res = [];
  5. // transfer control (and message pass)
  6. var url1 = yield "http://some.url.2";
  7. var p1 = request( url1 ); // "http://some.url.1"
  8. // transfer control
  9. yield;
  10. data.res.push( yield p1 );
  11. },
  12. function*(data){
  13. // transfer control (and message pass)
  14. var url2 = yield "http://some.url.1";
  15. var p2 = request( url2 ); // "http://some.url.2"
  16. // transfer control
  17. yield;
  18. data.res.push( yield p2 );
  19. }
  20. );

In this formulation, the two generators are not just coordinating control transfer, but actually communicating with each other, both through data.res and the yielded messages that trade url1 and url2 values. That’s incredibly powerful!

Such realization also serves as a conceptual base for a more sophisticated asynchrony technique called CSP (Communicating Sequential Processes), which we will cover in Appendix B of this book.