This tutorial presumes some knowledge about the Result class covered here.
Coroutines are similar to subroutines, functions or methods, except that they have the ability to return multiple times. This means that coroutines have multiple exits and re-entries that allow execution to be suspended and later, resumed.
Coroutines have a long history in more academic programming circles, but have been gaining popularity as of late, in particular for their usefulness with multi-tasking. .NET has supported coroutines since the release of C# 2.0, except that they are called iterators.
Here is simple example of a structure for a linked list of Nodes supporting iteration over the set:
public class Node : IEnumerable<Node> {
public Node Next;
public IEnumerator<Node> GetEnumerator() {
Node current = this;
while( current != null ) {
yield return current;
current = current.Next;
}
}
...
}
Now all nodes can be traversed with a regular foreach instead of manually calling and examining Next. The magic of Iterators comes from the yield keyword. A side-effect of returning IEnumerator is that once at least one yield exists, the method is considered to be an Iterator by the compiler and can not longer be exited using return. Instead, using yield return, execution of the enumerator is suspended and the returned value placed into IEnumerator.Current. The method is resumed when IEnumerator.MoveNext() is called. This continues until execution reaches the end of the method or yield break is called.
However, there is nothing to keep us from using the yield for our own purposes and manually calling IEnumerator.MoveNext() and retrieving the result from IEnumerator.Current to walk through the multiple entry points of a method:
public IEnumerable MultipleReturns() {
Console.WriteLine("** doing some initial work, then yielding execution context **");
yield return "first result";
Console.WriteLine("** doing some more work, then yielding execution context again **");
yield return "second result";
Console.WriteLine("** finish up the work and exit **");
yield break;
}
For simplicity, we’ll illustrate the use of this method assuming we know how many entrypoints exist:
public void ExecuteMultipleReturns() {
Console.WriteLine("create coroutine");
IEnumerator results = MultipleReturns().GetEnumerator();
Console.WriteLine("start execution");
bool hasResult = results.MoveNext();
Console.WriteLine("Is there a result? {0} => {1}", hasResult, results.Current);
Console.WriteLine("do something then return execution context to coroutine");
hasResult = results.MoveNext();
Console.WriteLine("Is there a second result? {0} => {1}", hasResult, results.Current);
Console.WriteLine("do something else, then return context so corouting can finish");
hasResult = results.MoveNext();
Console.WriteLine("is there another result? {0}",hasResult);
Console.WriteLine("done");
}
this results in the following output:
create coroutine start execution ** doing some initial work, then yielding execution context ** Is there a result? True => first result do something then return execution context to coroutine ** doing some more work, then yielding execution context again ** Is there a second result? True => second result do something else, then return context so corouting can finish ** finish up the work and exit ** is there another result? False
Now let’s return to our friend Result and see how we can use it along with the Iterator pattern. Suppose you want to use Result to wait for a value via another Result:
public void UsingResultToReceiveData() {
Result<int> result = new Result<int>();
Result<Result<int>> trigger = new Result<Result<int>>();
trigger.WhenDone(GetResult);
trigger.Return(result);
int r = result.Wait();
}
public void GetResult(Result<Result<int>> inner) {
inner.Value.Return(42);
}
We use a Result object to be able to trigger our request and then block on the our main Result<int> until GetResult returns our result (Note that the order of trigger.WhenDone() and trigger.Return() is irrelevant). Now let’s see how this code can be made simpler by using Dream’s Coroutine helper and yield:
public void IntroducingCoroutine() {
Result<int> result = new Result<int>();
Coroutine.Invoke(GetResult, result);
int r = result.Wait();
}
Yield GetResult(Result<int> result) {
yield return Async.Sleep(TimeSpan.FromSeconds(2));
result.Return(42);
yield break;
}
Notice how the call has gotten significantly simpler. We got rid of the Result we used to trigger the call and simply pass in our result to Coroutine.Invoke. On the Result generation side, we no longer return void, but instead return a MindTouch Dream construct, called Yield, which is simply an alias for IEnumerator<IYield>. We also added a superflous yield return, just to illustrate that we can have multiple yields, and those yields could be used to kick off asynchronous operations that GetResult will suspend for.
The use of IEnumerator<IYield>, or the alias Yield is required to make the code compatible with the coroutine framework in MindTouch Dream. Usually code using the coroutine framework will have the following declaration to create the alias after namespace declaration:
using Yield = IEnumerator<IYield>;
The IYield interface is defined in the MindTouch namespace. It indicates that something can be executed in steps. There are some limitations to what can be described in coroutines. However, these limitations are no different from those imposed by asynchronous programming in general. For instance, methods cannot return values or have out/ref parameters. This limitation is part of the design guidelines for asynchronous code, but for coroutines the compiler will actually enforce it.
Important: There is a very important detail that must be observed. Coroutines will be partially executed when invoked. In order to for the invocation to complete, the call must tie back into the iterative programming framework.
Now that we can invoke a coroutine which can yield to a number of asynchronous processes, another possibility emerges: continuing execution after the call has already returned to the invokee. This is often useful when a task requires some clean-up to be run after the result is already determined. By using the coroutine, the invokee can receive the result and continue on without being blocked by the clean-up still happening in the coroutine.
As a final example, let’s fetch a web page asynchronously, return its content length to the invokee and save the retrieved page to disk in the background:
Yield GetContentLength(XUri uriToDownload, Result<int> result) {
Plug p = Plug.New(uriToDownload);
Result<DreamMessage> res;
yield return res = p.GetAsync();
// return the result
result.Return((int)res.Value.ContentLength);
yield return result;
// copy the stream to disk in the background
yield return Async.CopyStream(
res.Value.AsStream(),
File.OpenWrite(Path.GetTempFileName()),
res.Value.ContentLength,
new Result<long>());
}
This can be called with (including timeout):
Result<int> result = new Result<int>(TimeSpan.FromSeconds(5));
Coroutine.Invoke(GetContentLength, new XUri("http://www.mindtouch.com"), result);
As you can see coroutines allow for some fairly complex asynchronous constructs with a relatively simple and synchronous looking syntax.
| Images 0 | ||
|---|---|---|
| No images to display in the gallery. |