Up Batch asynchronous tasks with coroutines

Serial coroutine flow

Last modified on August 15, 2012 17:20

This topic diagrams the flow of control for a serial coroutine call. 

The actors in the diagram are:




Caller: your code that calls the Coroutine.Start  method and monitors the CoroutineOperation  object (shown in the diagram as "Coroutine Result")

Producer: your code that provides the sequence of synchronous and asynchronous statements you want executed as a single unit of work. It's the primary argument your caller provides to the Coroutine.Start method. If you write ,Coroutine.Start(LoadCustomerInfoCoroutine), then LoadCustomerInfoCoroutine is your Producer.

The Producer is usually implemented as a .NET Iterator. After each asynchronous operation, your iterator yields a DevForce INotifyCompleted object such as the OperationBase-derivative returned by a DevForce asynchronous operation. For example, if the asynchronous task were a query such as Manager.Customers.ExecuteAsync(); your iterator would yield return the EntityQueryOperation<Customer> object returned by that query method.

Consumer: A DevForce Coroutine "Consumer" co-routine works with the "Producer" to move forward through the sequence of asynchronous serial tasks. The Consumer and Producer are partners: the "Consumer" manages the flow, the "Producer" provides the tasks.

You won't actually see the Consumer coroutine ... it's a hidden by-product of the DevForce Coroutine.Start(producer) call. But it's helpful to know that something is there, working with your Producer to make the magic happen.

You will see numbered balls in the diagram. These are the INotifyCompleted objects (aka, "Coordinator" objects) that are passed around among the Caller, Consumer and Producer.

The asynchronous operations themselves are also represented by the numbered balls; when they complete, they make changes to the Coordinator objects such as setting the results, error flags, cancellation flags ... and most importantly, they both raise the Completed event and invoke the Consumer callback function that tells the Consumer to take the next evaluation step.


A Serial Coroutine Story in Pictures

The Caller asks the Coroutine to start. It hands back a CoroutineOperation object (aka "Coordinator") containing information about task processing and, ultimately, the disposition of the asynchronous operations - the tasks - encapsulated by the Coroutine "Producer".

The CoroutineOperation object has a Completed event. The Caller typically adds a handler to this Completed event; the handler will know what to do when all of the co-routine tasks have finished.

CoroutineFlow01.png

The DevForce Coroutine class consumes notifications yielded by the "Producer". It commands the Producer to give it the next notification.

Typically, a Producer is an Iterator function that returns an IEnumerable<INotifyCompleted>. The consumer extracts from this function an Enumerator (an implementor of the IEnumerator interface) which offers "MoveNext()" and "Current" members. These members combine to fulfill the "Get next" command semantics.

CoroutineFlow02.png

The Producer does some work. It encounters an asynchronous method call. It yields the OperationBase object returned by the asynchronous method (e.g., an EntityQueryOperation<Customer> object from a query such as Manager.Customers.ExecuteAsync()).

OperationBase implements INotifyCompleted and is thus the kind of "Coordinator" object that the DevForce Coroutine Consumer expects.

Note that the asynchronous operation has not completed yet (it's still gray). The Producer is done for the moment. It is suspended until the Consumer asks for the next INotifyCompleted object.

CoroutineFlow03.png

The DevForce Coroutine Consumer adds the received INotifyCompleted object to the CoroutineOperation.Notifications collection.

The DevForce Coroutine Consumer stops at this point. In effect it suspends further processing It won't ask the Producer for the next INotifyCompleted object .. until "reawakened" by a callback from the "Coordinator" object - the INotifyCompleted object just yielded by the Producer.

The application continues to run. The two co-routines - Producer and Consumer - are poised to continue to the next task. But neither makes a move until the pending asynchronous operation completes ... and calls the Producer.

CoroutineFlow04.png

The asynchronous operation finishes at last, raising its Completed event and signaling success (It's green!). Because the asynchronous operation object implements INotifyCompleted, it also calls back to the DevForce Coroutine Consumer.

The reawakened Consumer sees that the asynchronous operation returned successfully ...

CoroutineFlow05.png

... so the Consumer asks the Producer for the next INotifyCompleted "Coordination" object.

CoroutineFlow06.png

The cycle repeats:

  • The Producer does some work until it encounters another asynchronous function.
  • It yields that asynchronous function's returned value, an INotifyCompleted object. 
  • The Consumer adds that object to the Notifications list of the CoroutineOperation object (shown as "Coroutine Result" in the diagram)
  • The asynchronous operation eventually succeeds, raises its Completed event and calls the Consumer to tell it about that success.

CoroutineFlow07.png

After three more cycles the Producer indicates that has no more Notifications to yield (the MoveNext() method returns false).

CoroutineFlow08.png

The DevForce Coroutine Consumer raises its own Completed event.

The Caller has been listening for this event and now knows that the Coroutine has completed all of its tasks successfully.

More precisely, the handler that the Caller attached to the CoroutineOperation's Completed event is ready to interpret the outcome of the co-routine tasks.

The handler can inspect the CoroutineOperation object's Notifications collection and examine any of the asynchronous tasks that were completed.

Usually it only cares about the last one. The handler will often extract results from the last completed operation or perhaps from the CoroutineOperation object itself.

It's really up to the handler what happens next; the co-routines have done their jobs.

CoroutineFlow09.png

Failure

Perhaps something went wrong with the second asynchronous operation. It raises its Completed event but its EventArgs indicate an error. The asynchronous operation calls back to the DevForce Coroutine Consumer - just as it would if the operation had succeeded.

CoroutineFlow10.png

The DevForce Coroutine Consumer sees the error. It observes that the error was not handled* so it concludes that it should stop asking the Producer for more notifications. It won't call the Producer's MoveNext() method again. Instead, it raises the Completed event of the CoroutineOperation object ("Coroutine Result" in the diagram).

The Caller's handler is listening to that event. The Caller's handler sees that there was a failure and must decide what to do next.

If the Caller's handler neglects to call "MarkAsHandled()" on the CoroutineOperation object, the DevForce Coroutine will throw the exception.

CoroutineFlow11.png

* The"Producer" could have attached its own Completed handler to the asynchronous operation object. The Producer's Completed handler gets the first look ... before the Coroutine sees the Completed event. If the Producer's handler marks the asynchronous operation object as handled (e.g., by calling MarkAsHandled()), then the Coroutine Consumer will resume the process and ask the Producer for the next Notification object.

Events versus Callbacks

We've told this story as if Completed events and event handlers were "the way to go". Most developers prefer events. But the Coroutine and asynchronous methods all support callback delegates. You are welcome to use callbacks instead of the Completed events.

Tags: Coroutine
Created by DevForce on October 13, 2010 15:59

This wiki is licensed under a Creative Commons 2.0 license. XWiki Enterprise 3.2 - Documentation. Copyright © 2015 IdeaBlade