- Silverlight Best Practices: Asynchronous Programming
- Using Action for an Event Façade
- Using the Asynchronous Programming Model (APM)
- The Problem: Nested Service Calls
- Summary
The Problem: Nested Service Calls
The problem with using this method becomes evident when you must nest several calls and wait for the result of one call to complete before the next call is made. For example, consider a block of code that must add numbers to the result of previous calls. The next addition cannot take place until the previous result returns. Here is the code it takes to add three numbers asynchronously with exception handling:
public void AddThreeNumbers() { Add(1, 2, _AddThreeNumbersStepTwo); } private void _AddThreeNumbersStepTwo(Exception exception, int result) { if (exception != null) { throw exception; } Add(result, 3, _AddThreeNumbersStepThree); } private void _AddThreeNumbersStepThree(Exception exception, int result) { if (exception != null) { throw exception; } Debug.WriteLine("1 + 2 + 3 = {0}", result); }
That’s a lot of code to perform a simple set of operations that is basically a sequential asynchronous workflow. You could try to simplify this by using lambda expressions and ignoring any exceptions:
public void AddThreeNumbersShorter() { Add(1, 2, (e,r)=> Add(r, 3, (ex,result) => Debug.WriteLine("1 + 2 + 3 = {0}", result))); }
Obviously that takes less space but is more confusing to read and less easy to maintain. That is just with two sets of numbers to add-imagine how complex it could become with more steps! Fortunately there are two solutions that can help you organize asynchronous operations in your code.
Solution 1: The State Machine
The state machine solution uses the current .NET Framework and the iterator function to create an asynchronous workflow. Anytime you code a foreach loop, you are using an iterator. An iterator is simply a state machine that keeps track of the current state and allows you to transition to the next state. In C#, you can supply that state using the yield keyword. You can think about yield as a special return that allows the method to be re-entered. Instead of always entering the method at the beginning, code will be executed until the next iteration is required, and then will return after the previous yield statement.
To see this in effect, take a look at this simple console program:
class Program { static void Main(string[] args) { Console.WriteLine("Before Enumerating..."); foreach(var str in Strings()) { Console.WriteLine("Inside the loop: {0}", str); } Console.ReadLine(); } public static IEnumerable<string> Strings() { var x = 1; Console.WriteLine("Starting out..."); yield return "First String"; x++; Console.WriteLine("After the first string"); yield return "Second String"; x++; Console.WriteLine("After the second string."); yield return "Last String"; x++; Console.WriteLine("After the last string x is {0}", x); } }
When you run the program, you receive the following output:
Before Enumerating... Starting out... Inside the loop: First String After the first string Inside the loop: Second String After the second string. Inside the loop: Last String After the last string x is 4
This gives you an idea of how the iterations re-enter the method and continue execution where the last statement exited. This can be used to create asynchronous workflows. Any asynchronous operation follows the pattern of executing (begin) followed by returning with a result (end). This can be illustrated through a simple interface:
public interface IAsynchronousStep { Action Begin { get; set; } Action End { get; set; } }
To provide a workflow, you implement an iterator for the interface. The iterator calls begin, waits for the end call to take place, and then iterates to the next step. The controller for this can be written using the following code. Note that it simply creates an enumerator to iterate the collection, captures the next item, and calls the method to start it. The item is responsible for performing the asynchronous task and then calling the end method when done. The end method is set by the controller to iterate to the next item.
public class AsynchronousWorkflow { private readonly IEnumerator<IAsynchronousStep> _enumerator; private AsynchronousWorkflow(IEnumerable<IAsynchronousStep> workflow) { _enumerator = workflow.GetEnumerator(); } private void _Next() { if (!_enumerator.MoveNext()) return; var next = _enumerator.Current; next.End = _Next; next.Begin(); } public static void Begin(IEnumerable<IAsynchronousStep> workflow) { new AsynchronousWorkflow(workflow)._Next(); } }
Now you can implement the interface for the service call:
public class AddStep : IAsynchronousStep { private readonly MyServicesClient _client = new MyServicesClient(); public AddStep(int x, int y) { Begin = () => _client.AddAsync(x,y); _client.AddCompleted += Completed; } public void Completed(object sender, AddCompletedEventArgs args) { Result = args.Result; _client.AddCompleted -= Completed; End(); } public int Result { get; private set; } public Action Begin { get; set; } public Action End { get; set; } }
The implementation keeps track of a client and subscribes to the result. When the result is received, it unsubscribes and sets the result. This allows you to perform the workflow like this-while there was a bit of effort to set up the infrastructure, you can see that the code below is far easier to read, understand, and maintain-and because it uses the iterator state machine, it will not block any threads while waiting for the asynchronous steps to complete:
public void AddThreeNumbers() { AsynchronousWorkflow.Begin(Addition()); } public static IEnumerable<IAsynchronousStep> Addition() { var firstStep = new AddStep(1, 2); yield return firstStep; var secondStep = new AddStep(firstStep.Result, 3); yield return secondStep; Debug.WriteLine("1 + 2 + 3 = {0}", secondStep.Result); }
The fully functional version of this solution requires exception handling and more control over the types of steps you can pass in. To see the full solution, visit the Jounce project. Jounce is an open-source MVVM framework for Silverlight, and provides a workflow implementation that allows you to wrap events, routed events, and background workers, and even use generic delegates to perform asynchronous, sequential workflows using state machines.
Solution 2: Await
A more robust solution will be provided with the next version of Visual Studio. The solution is available now as a Community Technology Preview (CTP) called the Async CTP. You can download the latest version on the Microsoft website.
The solution does not change the framework in any way, but introduces a new keyword that facilitates asynchronous tasks. This keyword is the await keyword. The easiest way to think of the keyword is as a yield keyword designed specifically for asynchronous tasks. The compiler will generate the necessary code to wrap the calls so they perform asynchronously and do not block the main thread.
The await keyword works in conjunction with the new Task type that was introduced with the .NET Framework 4.0. The new type was introduced to make it easier for developers to work with multi-threaded, parallel, and/or asynchronous tasks. Using the new keyword is a two-step process.
The first step is to define the signature of the task. You’ll find the signature is very similar to the asynchronous steps used in the previous solution. Instead of an end method, the method creates a special “completion source” that is set when the task is finished:
public Task<int> AddNumbers(int x, int y) { var client = new MyServicesClient(); TaskCompletionSource<int> result = new TaskCompletionSource<int>(); client.AddCompleted += (o, e) => { result.TrySetResult(e.Result); }; client.AddAsync(x,y); return result.Task; }
The completion source is typed to the result, and will not “complete” the task until it is updated with an exception or a result. The method then subscribes to the client to update the task with the result of the asynchronous web service call, initiates the call, and returns the corresponding task.
Now that the task is set up, using it is even simpler than the previous solution because it does not have to be wrapped in an iterator. Invoking it is as simple as:
public void AddNumbersWithAwait() { var result = await AddNumbers(1, 2); result = await AddNumbers(result, 3); Debug.WriteLine("1 + 2 + 3 = {0}", result); }
As you can see, this is by far the easiest and most straightforward method to use and results in lean, easy to read, and simple to maintain code.