CCR Iterators
Glossary Item Box
Microsoft Robotics Developer Studio | Send feedback on this topic |
CCR Iterators
Iterators are a C# 2.0 language feature that is used in a novel way by the Concurrency and Coordination Runtime (CCR). Instead of using delegates to nest asynchronous behavior, also known as callbacks, you can write code in a sequential fashion, yielding to CCR arbiters or other CCR tasks. Multi-step asynchronous logic can then be all written in one iterator method, vastly improving readability of the code. At the same time maintaining the asynchronous behavior, and scaling to millions of pending operations since no OS thread is blocked during a yield.
(Iterators can be used to step through objects like lists, arrays, etc. and on each call to the iterator the next item is returned until finally there are no more left. CCR iterators return a series of tasks to be executed, pausing between tasks as necessary. In this way, what appears to be sequential code is actually a series of small code snippets, or thunks, that are executed one after another, possibly on different threads. Note that a thread is never blocked during the execution of a CCR iterator. When an iterator needs to wait on a port, it returns the thread to the pool and another thread is scheduled later to execute the next piece of the code when a message arrives on the port.)
Example 18
void StartIterator()
{
// create an IterativeTask instance and schedule it
Arbiter.Activate(_taskQueue,
Arbiter.FromIteratorHandler(IteratorExample)
);
}
/// <summary>
/// Iterator method scheduled by the CCR
/// </summary>
IEnumerator<ITask> IteratorExample()
{
// accumulator variable
int totalSum = 0;
var portInt = new Port<int>();
// using CCR iterators we can write traditional loops
// and still yield to asynchronous I/O !
for (int i = 0; i < 10; i++)
{
// post current iteration value to a port
portInt.Post(i);
// yield until the receive is satisifed. No thread is blocked
yield return portInt.Receive();
// retrieve value using simple assignment
totalSum += portInt;
}
Console.WriteLine("Total:" + totalSum);
}
Example 18 shows how the StartIterator method uses the arbiter class to create a task from an iterator delegate and then submits it for scheduling using Arbiter.Activate. The second method is the iterator method. The iterator method can use the yield return and yield break C# statements in its body to control execution. What distinguishes this method from more common C# methods is the return value:
IEnumerator<ITask> IteratorExample()
{
The return value indicates this is a C# iterator over CCR ITask instances and informs the compiler that the method body might contain yield statements.
yield return portInt.Receive(); {
The yield return statement above returns control to the CCR scheduler. It also returns an instance of the ITask interface, implemented by the ReceiverTask created when calling the portInt.Receive extension method. The CCR scheduler activates the returned task and also associates the iterator with the task. When the task completes execution, it can choose to progress the iterator to the next code statement after the yield. When you think about this code, you can think of the yield as a synchronization point: the iterator method will stop execution until the yield is satisfied.
Note that when code execution resumes at the line after the yield return, there is no guarantee that it will be the same CCR thread. Also, you cannot use the yield return statement in normal code, only within an Iterator. To terminate execution of the iterator early without "falling off the end" you can use the yield break statement. Iterators cannot return a value in the sense of a normal method, so they need to modify variables outside their scope, or preferably post a message to a port (passed in as a parameter) with the return value.
Never yield to persisted receivers within an iterator. In the yield statement above, Receive creates a one time receiver. If the receiver is persisted, the yield is never considered satisfied, so the iterator will never progress to the next statement after the yield |
An alternative way to schedule an iterative task is to use SpawnIterator(). Just like Spawn() for conventional methods, there are three overloads of SpawnIterator that accept 1, 2, or 3 parameters that are passed to the iterative task, e.g. SpawnIterator<T0>(T0, Handler<T0>). It is important to note that you cannot call an iterative task from normal code. Doing so will not generate any errors, but it will have no effect. If you want to execute an iterative task, you must use SpawnIterator() or execute it indirectly through an Arbiter. (See below).
Implicit parameter passing through local variables
The power of using the CCR with the C# iterators comes from two other C# language features:
- Anonymous methods: This feature is used in our examples to define the body of a handler as an in-lined delegate.
- The compiler captures all local variables in the iterator method that are referenced inside the body of the anonymous method. This allows the delegate to use local variables, defined in the parent method. The delegates, that always run in some other thread, can communicate results back to the parent iterator, with no explicit parameter passing.
// retrieve value using simple assignment
FakePre-3bec44a236994a75b37ce0fcc9ec0bd1-a3196cf7dbc84101adcd6392ff179d5e
The next statement after the yield is shown above. Using an implicit assignment operator, the code extracts the value from the port. Since the receive operation above was satisfied, and no handler was passed as an argument to portInt.Receive(), the item was kept in the port.
Console.WriteLine("Total:" + totalSum);
The line above is from the body of the iterator method IteratorExample, used in example 18. Its the last statement in the method and it implicitly marks the end of the iteration.
Yielding to coordination primitives
Example 19
void StartIterator2()
{
Port<string> portString = new Port<string>();
Arbiter.Activate(
_taskQueue,
Arbiter.ReceiveWithIterator(false, portString, StringIteratorHandler)
);
}
IEnumerator<ITask> StringIteratorHandler(string item)
{
Console.WriteLine(item);
yield break;
}
Example 19 shows how to specify an iterator method as the outcome of a receive operation using ReceiveWithIterator. Until now the examples have been using traditional methods that execute when an arbiter is satisfied. The arbiter class contains methods that expect an iterator delegate for all the major coordination primitives, JoinReceiver, MultipleItemGather, Choice, Receiver, etc. This gives you the choice between regular methods or iterator methods for every CCR coordination primitive. The arbiter implementations work with instances of ITask, which hides the type of the user delegate, so they work with iterator or non-iterator methods.
Example 20
IEnumerator<ITask> IteratorWithChoice()
{
// create a service instance
ServicePort servicePort = ServiceWithInterleave.Create(_taskQueue);
// send an update request
UpdateState updateRequest = new UpdateState();
updateRequest.State = "Iterator step 1";
servicePort.Post(updateRequest);
string result = null;
// wait for either outcome before continuing
yield return Arbiter.Choice(
updateRequest.ResponsePort,
response => result = response,
ex => Console.WriteLine(ex)
);
// if the failure branch of the choice executed, the result will be null
// and we will terminate the iteration
if (result == null)
yield break;
// print result from first request
Console.WriteLine("UpdateState response:" + result);
// now issue a get request
GetState get = new GetState();
servicePort.Post(get);
// wait for EITHER outcome
yield return Arbiter.Choice(
get.ResponsePort,
delegate(string response) { result = response; },
delegate(Exception ex) { Console.WriteLine(ex); }
);
// print result from second request
Console.WriteLine("GetState response:" + result);
}
Example 20 shows a common use case for iterators: When you require multiple asynchronous requests to a service, where the outcome of each request is success or failure. In real world examples there is usually some data dependency of request N, from the result of request N-1, which is why they are done in sequence. This is the ability to yield to the return value of Arbiter.Choice, which creates an instance of the Choice arbiter. Allowing the iterator to concisely handle multiple outcomes of an asynchronous I/O operation, all in-line. Note that the iterator method continues execution when either branch of the choice executes. It also uses the result stack variable to retrieve the result of the requests.
Nesting of iterators
When an iterator methods grows too large to be easily maintainable, it can be decomposed further to smaller iterator methods. The CCR scheduler can determine when an iterator has exhausted all its steps. This means the iterator reached a yield break or finished executing the last step of the iteration.
Example 21
IEnumerator<ITask> ParentIteratorMethod()
{
Console.WriteLine("Yielding to another iterator that will execute N asynchronous steps");
yield return new IterativeTask<int>(10, ChildIteratorMethod);
Console.WriteLine("Child iterator completed");
}
IEnumerator<ITask> ChildIteratorMethod(int count)
{
Port<int> portInt = new Port<int>();
for (int i = 0; i < count; i++)
{
portInt.Post(i);
// short form of receive that leaves item in port
yield return portInt.Receive();
// implicit operator extracts item from port
int result = portInt;
}
}
Example 21 shows the nesting of iterators by simply creating a new IterativeTask and yielding to it:
- The parent iterator method yields execution to the new IterativeTask instance.
- CCR schedules the IterativeTask instance on the dispatcher queue instance used by the current iterator. Arbiter.FromIteratorHandler can also be used instead of explicitly creating an IterativeTask .
- The child iterator method executes in one of the dispatcher threads, and yields 10 times to a simple receive operation. A loop is used to showcase how iterators make looping around asynchronous operations very easy and readable. It also shows that the parent iterator can yield to arbitrarily complex child iterators, without knowing how many asynchronous steps they execute.
Note that you can yield to regular tasks, by using the Arbiter.ExecuteToCompletion method.
If an exception is thrown in a handler executed in the context of a nested iterator, the parent iterator will still be called. Exceptions implicitly terminate iterators and the CCR properly disposes them. If causalities as present, the exception will be posted on the causality exception port, then the iterator will be disposed |
© 2012 Microsoft Corporation. All Rights Reserved.