My last post made the case for building async services in .NET 4.5 with the Task-based Asynchronous Pattern, or TAP for short. I profiled an ASP.NET Web API service that uses synchronous methods, showing how long-running services with high throughput result in the production of extra threads and the associated overhead carried by all those threads – both in terms of memory (1 MB stack space per thread) and CPU utilization (context switching). However, thanks to Tasks and their support in the C# programming language with the async and await keywords (also included with VB.NET), building asynchronous services with the ASP.NET Web API is straightforward and relatively painless.
Download the code for this post here.
The idea is to avoid blocking the service thread when performing IO-bound asynchronous operations, such as accessing the network, file system, or a database. In these and similar scenarios, a a dedicated thread is not required while the device performs its work. Luckily, many API’s have been updated to include Task-based methods, making it easy to await them from within an async service operation. This includes Entity Framework 6 (available at the time of this writing as a pre-release NuGet package), HttpClient (for calling other HTTP-based services), and WCF 4.5 (for SOAP-based services).
Adapting EAP Methods
I also described how you can use Task.Factory.FromAsync to convert legacy Begin/End methods from the APM pattern to a Task you can await on. In addition, TAP provides a class called TaskCompletionSource, which lets you take any API and wrap it in a Task and affect its status (Cancelled, Faulted, or RanToCompletion, etc). Here you can adapt non-async operations, such as reading from a queue or responding to Timer ticks, to simulate IO-bound Task-based operations. Here is an example of using TaskCompletionSource to adapt the Event Async Pattern to a Task by setting the task state in the event handler for a WebClient.
public async Task<string> WebServiceEventAsync(int id) { // Wire up event handler to task completion source var address = string.Format("{0}{1}", WebServiceAddress, id); var client = new WebClient {BaseAddress = address}; var tcs = new TaskCompletionSource<string>(); client.DownloadStringCompleted += (s, e) => { if (e.Cancelled) tcs.TrySetCanceled(); else if (e.Error != null)tcs.TrySetException(e.Error); else tcs.TrySetResult(e.Result); }; // Run async method and await tcs's task client.DownloadStringTaskAsync(address).NoWarning(); string json = await tcs.Task; var result = JsonConvert.DeserializeObject<string>(json); return result; }
NoWarning is an extension method you can use to suppress the compiler warning for calling an awaitable method without awaiting on it.
public static class TaskExtensions { // Silences compiler warning public static void NoWarning(this Task task) { } }
Help Page and Test Client
It’s easy to test ASP.NET Web API operations without creating a manual test client. First, make sure to install the ASP.NET and Web Tools 2012.2 Update, so that you get the Web API help page.
Then, add the WebApiTestClient NuGet package to your project, and add the following lines to Areas/HelpPage/Views/Help/Api.cshtml:
@Html.DisplayForModel("TestClientDialogs") @section Scripts { <link type="text/css" href="~/Areas/HelpPage/HelpPage.css" rel="stylesheet" /> @Html.DisplayForModel("TestClientReferences") }
Select an operation, then click Test API, enter parameter values and/or a body, and click Send.
The response will appear in a friendly box.
Multiple Downstream Services
Now that we’ve looked at adapting APM methods to TAP with TaskCompletionSource, let’s have a little more fun with a service that calls multiple downstream services. You might, for example, want to call two or more other services sequentially from within your async Web API service. Because await obviates the need for continuation or callback methods, it’s simply a matter of awaiting the first service call, then immediately awaiting the second service call.
public async Task<string> SequentialServicesAsync(int id) { // Start tasks sequentially int id1 = id; int id2 = id1 + 1; var results = new string[2]; results[0] = await WebServiceAsync(id1); results[1] = await SoapServiceAsync(id2); var result = string.Join(" ", results); return result; }
The C# compiler magically transforms the code after the first await into a non-blocking continuation. The same thing is done with code following subsequent awaits, all without nested lambdas.
This pattern is useful for scenarios where you might predicate the second service operation on results from the first, in which case you wouldn’t want to begin the second operation until the first has completed. There are cases, however, when you would instead want to call multiple downstream operations in parallel. Your friends here are the combinator methods, Task.WhenAll and Task.WhenAny. Use Task.WhenAll when you want to process the results after all the operations have completed; use Task.WhenAny when you want to process each operation as it completes. Here is an example of the latter.
public async Task<string> ParallelServicesAsync(int id) { // Start tasks in parallel (with delay) int id1 = id; int id2 = id1 + 1; var task1 = SoapServiceAsync(id2); await Task.Delay(TimeSpan.FromSeconds(3)); var task2 = WebServiceAsync(id1); // Harvest results as they arrive (in lieu of Task.WhenAll) var results = new List<string>(); var tasks = new List<Task<string>> { task1, task2 }; while (tasks.Count > 0) { Task<string> task = await Task.WhenAny(tasks); tasks.Remove(task); string r = await task; Debug.Print("Response received: {0}", r); // process results.Add(r); } var result = string.Join(" ", results); return result; }
First, we start each task, with a slight delay to offset execution. Then we iteration over the list of tasks, calling Task.WhenAny to get the first task that has completed, so we can remove it from the list and process the result.
Lastly, we’ll look at a scenario where we want to retry an individual operation a specific number of times, aborting only when the maximum number of retries has been met.
public async Task<string> RetryServiceAsync(int id) { // Create http client that has a timeout var client = new HttpClient { BaseAddress = new Uri(WebServiceAddress), Timeout = TimeSpan.FromSeconds(3) }; string result = null; const int maxRetries = 3; for (int i = 0; i < maxRetries; i++) { try { // Succeed only on last attempt if (i == maxRetries - 1) client = new HttpClient { BaseAddress = new Uri(WebServiceAddress), Timeout = TimeSpan.FromSeconds(6) }; string json = await client.GetStringAsync(id.ToString()); result = JsonConvert.DeserializeObject<string>(json); break; } catch (Exception ex) { Debug.Print("Attempt {0} Error: {1}", i + 1, ex.Message); // log if (i == maxRetries - 1) throw new Exception(string.Format("Max retries of {0} exceeded", maxRetries)); } } return result; }
Here we see how much easier async exception handling is made with the await keyword, because the exception is handled in the continuation that takes place after the await. In this example, we create an HttpClient with a 3 second timeout. Because the service operation is coded to take at least 5 seconds (not shown here), the call to client.GetStringAsync will fail with an OperationCancelledException, in which case we simply log the error and continue the retry loop, or throw an exception if we’ve exceeded the max retries. We can avoid that by increasing the timeout to 6 seconds for the last iteration – we can also comment out that code to see the retry max exceeded.
The important thing to note about each of these examples is that we are not blocking the service thread when calling multiple IO-bound async operations. When we await an async service operation, the remainder of the method is re-written by the compiler as a callback, which in this case will execute on a Thread Pool thread. But during the time in which the downstream service operation is executing, our service thread will quickly exit, so that it can be used for another incoming service request. That is an essential ingredient for scalable services that can handle high-latency IO with high levels of throughput while creating the minimum number of threads needed to service each request.
Pingback: WebAPI Async – .NET