CHAPTER 7

image

async and await

In view of Windows 8’s mantra of “Fast and Fluid,” it has never been more important to ensure that UIs don’t stall. To ensure that UI developers have no excuses for not using asynchronous methods, the C# language introduces two new keywords: async and await. These two little gems make consuming and composing asynchronous methods as easy as their synchronous counterparts.

This chapter is a story of two parts. The first part explains how easy asynchronous programming can be in C# 5, while the second part is a deep dive into the compiler magic behind the keywords. The second part may not be for everyone, so feel free to skip it.

Making Asynchronous Programming Simpler

The code in Listing 7-1 represents a UI button handler that, when clicked, performs some calculation and updates the UI with the result. Assuming the CalculateMeaningOfLife method doesn’t take too long to execute, life is good. If it were to take a noticeable amount of time, we would be encouraged to create an asynchronous form of the method for the UI developer. The UI developer would then combine this with a continuation on the UI thread to update the UI.

Listing 7-1.  UI Event Handler Performing Some Processing

private void ButtonClick(object sender, RoutedEventArgs e)
{
  try
  {
      decimal result = CalculateMeaningOfLife();
 
      resultLabel.Text = result.ToString();
   }
   catch (MidlifeCrisisException error)
   {
      resultLabel.Text = error.Message;
   }
}

Listing 7-2 achieves the goal of keeping the UI responsive, but the style of programming has somewhat changed, becoming more complex. You now have to deal with a Task as opposed to just a simple decimal, and exception handling has changed to if/else checking.

Listing 7-2.  Keeping the UI responsive using CalculateMeainigOfLifeAsync

private Task<decimal> CalculateMeaningOfLifeAsync() { . . . }
 
private void ButtonClick(object sender, RoutedEventArgs e)
{
  Task<decimal> calculation = CalculateMeaningOfLifeAsync();
 
  calculation.ContinueWith(calculationTask =>
   {
      var errors = calculationTask.Exception as AggregateException;
 
       if (errors == null)
       {
          resultLabel.Text = calculation.Result.ToString();
       }
       else
       {
          Exception actualException = errors.InnerExceptions.First();
          resultLabel.Text = actualException.Message;
       }
   }, TaskScheduler.FromCurrentSynchronizationContext());
}

If developers are to be encouraged to embrace the use of asynchronous methods, then the programming model needs to be as simple as the synchronous model; this is one goal of the async and await keywords introduced in C#5. Taking advantage of these two new keywords, rewrite the code as shown in Listing 7-3.

Listing 7-3.  Utilising async/await to maintain synchronous structure

private Task<decimal> CalculateMeaningOfLifeAsync() { . . . }
 
private async void ButtonClick(object sender, RoutedEventArgs e)
{
  try
  {
    decimal result = await CalculateMeaningOfLife Async();
    resultLabel.Text = result.ToString();
  }
  catch (MidlifeCrisisException error)
  {
    resultLabel.Text = error.Message;
  }
}

Listing 7-3 does not block the UI; it continues to run CalculateMeaningOfLife asynchronously, but with the benefit of simplicity. It is identical in structure and type usage as the sequential version, and thus as easy to read as the original code laid out in Listing 7-1.

What Do async and await Actually Do?

Probably the most important thing to realize is that the async keyword does not make your code run asynchronously, and the await keyword doesn’t make your code wait. Other than that the keywords are pretty straightforward.

Joking aside, the async keyword does not influence the code generated by the compiler; it simply enables the use of the await keyword in the method. If the await keyword had been conceived when the C# language keywords were first defined in Version 1, there would never have been the need for async, as await would always have been a reserved word in the language. The problem with creating new language keywords is that there could be code out there that is using any potential new keyword as an identifier and as such would now not compile under the C# 5 compiler. So the async keyword is necessary simply to inform the compiler that when it sees the word await, you do mean the new language keyword.

In contrast with async, the await keyword does quite a lot. As just mentioned, it does not perform a physical wait. In other words,

decimal result = await CalculateMeaningOfLifeAsync();
        !=
decimal result = CalculateMeaningOfLifeAsync().Result;

as this would result in the calling thread entering a wait state until the asynchronous task had completed. This would then lock up the UI—the very thing you are trying to avoid. On the other hand, the thread cannot proceed until the result from the asynchronous operation is known; in other words, it can’t continue. What the compiler implements has the same intent as the code in Listing 7-2. It creates continuation (we will discuss the mechanics of this at the end of this chapter). In the current example all the code after the await statement forms a continuation, which runs when the asynchronous operation has completed, just like ContinuesWith. By having the compiler build the continuation for you, you have preserved the program structure.

The await statement consists of the await keyword and an asynchronous expression to wait upon. This expression is typically a Task; later, when we discuss the compiler mechanics in more detail, you will see it can potentially be other types as long as they follow certain conventions. Once the compiler has set up a continuation on the outcome of the asynchronous operation, it simply returns from the method. For the compiler to emit code that returns from the method without fully completing the method, the method must have a return type of one of the following:

  • void
  • Task
  • Task<T>

For methods returning void there is no return value, and hence the caller is unaware if the call has fully completed or just started. (Is this good or bad? More later.) In the cases where a Task is being returned, the caller is fully aware that the operation may not have fully completed and can observe the returned task to obtain the final outcome.

Hopefully you will have noticed that, although the CalculateMeaningOfLifeAsync method returns a Task<decimal>, the code using the await keyword is simply written in terms of decimal, so a further behavior of the await keyword is to coerce the result from the asynchronous expression and present it in the same form you would have gotten from a synchronized method call. Listing 7-4 shows a two-step version of the code in Listing 7-3 for clarity.

Listing 7-4.  Coercing the Task.Result

Task< decimal> calcTask = CalculateMeaningOfLifeAsync();
decimal result = await calcTask;

The await keyword therefore removes the need for having to deal with the Task directly.

For the UI to update successfully as part of your continuation, the continuation needs to run on the UI thread. At runtime the await behavior determines if SynchronizationContext.Current is non-null, and if so automatically ensures that the continuation runs on the same SynchronizationContext that was current prior to the await operation—again removing the need to write specific directives to continue running on the UI thread.

The final part of the story is error handling. Note that in Listing 7-3 you are now handling errors using a catch block, just as in the sequential code. The compiler, through the use of continuations, again ensures that the exception is caught and handled on the UI thread. However, you possibly may have thought that the new async/await code had a bug in it. After all if the CalculateMeaningOfLifeAsync() returns a task that ends in a faulted state, you would expect to receive an AggregateException. Just as the await coerces the return value from the task, it also coerces the underlying exception from the AggregateException. “Hold on,” you might say. “There may be many exceptions; hence the reason why Task uses AggregateException: await just simply takes the first.” Again, this is done to try and recreate the same programming model you would have with synchronized code.

To sum up: the async keyword just enables the use of await. The await keyword performs the following:

  • Registers a continuation with the asynchronous operation.
  • Gives up the current thread.
  • If SynchronizationContext.Current is non-null, ensures the continuation is Posted onto that context.
  • If SynchronizationContext.Current is null, the continuation is scheduled using the current task scheduler, which most likely will be the default scheduler, and thus schedules the continuation on a thread pool thread.
  • Coerces the result of the asynchronous operation, be it a good return value or an exception.

Last, it is worth noting that although async/await are heavily used for UI programming, they are also an effective tool for non-UI-based scenarios. You will see in Chapter 7 that they provide a convenient programming model for building composite asynchronous operations. Further, as opposed to waiting for asynchronous operations to complete, you will utilize continuations, reducing the overall burden on the thread pool.

Returning Values from async Methods

Async methods are expected to have at least one await operation, in order to be able to return from them early when a noncompleted task is encountered. If the method has no return value, then returning early is not a problem. If, however, the method returns a value, the method of returning early is a problem. Consider the synchronous code in Listing 7-5. As the signature of the method stands, you can’t simply add the async keyword—you need to change the method signature to return a Task<int> instead of just a simple int. This then informs the caller that the method will complete asynchronously, and hence to obtain the final outcome of the method they will need to observe the Task.

Listing 7-5.  Synchronous web page word count

public static int CountWordsOnPage(string url)
{
  using (var client = new WebClient())
  {
    string page = client.DownloadString(url);
 
    return wordRegEx.Matches(page).Count;
  }
 }

The asynchronous version of this method would therefore look as shown in Listing 7-6.

Listing 7-6.  Asynchrnous web page word count

public static async Task<int>CountWordsOnPageAsync(string url)
{
  using (var client = new WebClient())
  {
    string page = await client.DownloadStringTaskAsync(url);
 
    return wordRegEx.Matches(page).Count;
  }
}

The return type has changed to a Task<int> allowing the compiler the possibility to return early. However, note the return statement didn’t need to change; it still returns an int, not a Task<int>. The compiler takes care of that for you (as we will discuss later in the “async/await Mechanics” section). The client could utilize the method as follows:

CountWordsOnPageAsync("http://www.google.co.uk")
         .ContinueWith( wct => Console.WriteLine(wct.Result));

If all is good the caller will get their word count displayed on the screen. However, what if an exception fires inside the CountWordsOnPageAsync method? In the same way, the successful outcome of the method is communicated through the task, as are any unhandled exceptions. The Task returned from the method will now be in a completed state of faulted. When the caller observes the task result, the exception will be re-thrown and the caller will be aware of the error.

Now revisit the async method defined in Listing 7-3. It had a method signature of private async void ButtonClick(object sender, RoutedEventArgs e). Note that this method returns void, so there is no Task for the caller to observe. So what happens if the async method is terminated due to an unhandled exception? In this case the exception is simply re-thrown using the SynchronizationContext that was present at the point when the method was first called. This would then almost preserve the behavior of the synchronous version, delivering the exception on the UI thread. We say “almost” since wrapping the call to the method with a catch block won’t see the exception if it originates after the first await. The only place left to handle the exception is a top-level UI exception handler. If there is no SynchronizationContext then it is simply re-thrown and allowed to bubble up in the normal way; if no exception handler is found, the process will terminate.

Therefore, if the async method returns a Task, normal task exception-handling behavior applies. If it returns void, then the exception is simply re-thrown and allowed to bubble up.

You could perhaps argue that the designers of async and await should not have supported void methods and insisted on all methods returning a Task, as this has two effects:

  1. It makes the caller aware of the asynchronicity of the method.
  2. It ensures the caller is always able to fully observe the outcome of a specific invocation. The distance a catch block is from the cause often makes it harder or impossible to produce the correct level of recovery. Global handlers lack the local information necessary to produce an effective recovery, and thus are often only able to log and bring down the application gracefully.

The need to support void stems from allowing async and await to be used in event handlers—this is, after all, one of the key places where this pair of keywords will be used. However, I personally feel that it is good practice for all non–event-handler-based code that uses async and await to return a Task, enabling the caller to see it is asynchronous and giving them maximum opportunity to handle the outcome as close to the source as possible.

Should You Always Continue on the UI Thread?

Examine the code in Listing 7-7; it is being invoked from a UI thread.

Listing 7-7.  Synchronously load web page and remove adverts

private async void LoadContent(object sender, RoutedEventArgs e)
{
   var client = new WebClient();
   string url = "http://www.msdn.com";
 
   string pageContent = await client.DownloadStringTaskAsync(url);
                                          
    pageContent = RemoveAdverts(pageContent);
    pageText.Text = pageContent;
}

The page content is being downloaded asynchronously and thus not blocking the UI thread. When the asynchronous operation has completed, you will resume this method back on the UI thread to remove any adverts. Obviously if this remove method takes a long time it will block any UI processing. A bit of refactoring and you can do as shown in Listing 7-8.

Listing 7-8.  Asynchronously load web page and remove adverts

private async void LoadContent(object sender, RoutedEventArgs e)
{
    var client = new WebClient();
    string url = "http://www.msdn.com";
 
    string pageContent = await client.DownloadStringTaskAsync(url);
    pageContent = await Task.Run<string>(() => RemoveAdverts(pageContent));
 
    pageText.Text = pageContent;
 }

Voilà—you now have a version that doesn’t block up the UI. However, notice that you do end up back on the UI thread after the page has been downloaded, albeit for a short period of time to run a new task. Clearly you shouldn’t need to do that. You could solve it with a Task.ContinuesWith method call instead of a Task.Run (Listing 7-9).

Listing 7-9.  Continuing on a non UI thread using task continuations

private async void LoadContent(object sender, RoutedEventArgs e)
{
  var client = new WebClient();
  string url = "http://www.msdn.com";
 
  string pageContent = await client.DownloadStringTaskAsync(url)
                                   .ContinueWith(dt => RemoveAdverts(dt.Result));
  pageText.Text = pageContent;
}

This now means it will continue processing the page on a thread pool thread, and the UI thread is not involved until both those operations are completed. This is an improvement, but at what cost? The cost of being easy to read and easy to write. Async and await allow you to structure code as you would sequentially; with the code in Listing 7-9 you have moved back to your old Task-based API ways. The answer to this problem is twofold. First, you can configure the await operation so that it does not always continue back on the UI thread, but on a thread pool thread. To make that happen, call the ConfigureAwait method on the task, supplying a value of false as shown in Listing 7-10. (Shame this wasn’t an enum; lucky for you, you have named parameters.)

Listing 7-10.  Continuing on a non UI thread using ConfigureAwait

private async void LoadContent(object sender, RoutedEventArgs e)
{
  var client = new WebClient();
  string url = "http://www.msdn.com";
 
  string pageContent = await client.DownloadStringTaskAsync(url)
                                   .ConfigureAwait(continueOnCapturedContext: false);
            
  pageContent = RemoveAdverts(pageContent);
 
  pageText.Text = pageContent;
}

You have now removed the continuation, and the RemoveAdverts method is now running on a thread pool thread. So all is good . . . that is, until you come to the last line, which updates the UI. At this point, you obviously need to be back on the UI thread. This brings you to the second piece of refactoring to solve the problem. What you need to do is to turn the downloading and the removing of the adverts into a single task; then the LoadContent method can simply just await on that (Listing 7-11).

Listing 7-11.  Composite tasks utilising async/await

private async void LoadContent(object sender, RoutedEventArgs e)
{
   var client = new WebClient();
   string url = "http://www.msdn.com";
 
   string pageContent = await LoadPageAndRemoveAdvertsAsync(url);
    
   pageText.Text = pageContent;
}
 
public async Task<string> LoadPageAndRemoveAdvertsAsync(string url)
{
  WebClient client = new WebClient();
      
  string pageContent = await client.DownloadStringTaskAsync(url)
                                   .ConfigureAwait(continueOnCapturedContext: false);
 
  pageContent = RemoveAdverts(pageContent);
  return pageContent;
}

The LoadContent method now focuses on running 100 percent on the UI thread, delegating to the LoadPageAndRemoveAdvertsAsync method to run on whatever threads it needs to in order to get the job done. Once its job is done, the LoadContent method will resume on the UI thread. This latest refactoring has given you clean and efficient code.

Asyncandawaitoffer a convenient way to compose asynchronous operations, using simple conventional programming styles.

Task.Delay

.NET 4.5 introduces a series of additional Task class methods to assist in writing code that works with the await keyword. We will now review some of them and show you how to use them to great effect with async and await.

I’m sure we are all used to seeing code that blocks for a period of time before attempting an operation. For example, the code in Listing 7-12 is attempting an operation; if it fails, it will back off and try again up to three times.

Listing 7-12.  Synchronous back off and retry

for (int nTry = 0; nTry < 3; nTry++)
{
  try
  {
     AttemptOperation();
     break;
  }
  catch (OperationFailedException){}
  Thread.Sleep(2000);
}

While the code in Listing 7-12 works, when the execution hits Thread.Sleep it puts the thread to sleep for 2 seconds. This means the kernel will not schedule this thread to run for at least that period of time. While sleeping, the thread doesn’t consume any CPU-based resources, but the fact that the thread is alive means that it is still consuming memory resources. On a desktop application this is probably no big deal, but on a server application, having lots of threads sleeping is not ideal because if more work arrives on the server, it may have to spin up more threads, increasing memory pressure and adding additional resources for the OS to manage. Ideally, instead of putting the thread to sleep, you would like to simply give it up, allowing the thread to be free to serve other requests. When you are ready to continue using CPU resources again, you can obtain a thread (not necessarily the same one) and continue processing. In a multithreaded application, you will want to perform maximum concurrency with the minimum number of threads. We will cover the topic of efficient server side async in a lot more detail in a later chapter. For now you can solve this problem by not putting the thread to sleep, but rather using await on a Task that is deemed to be completed in a given period. As stated in Chapter 3, a Task represents an asynchronous operation, not necessarily an asynchronous compute. To create such a task, use Task.Delay, supplying the time period as in Listing 7-13.

Listing 7-13.  Thread efficient back off and retry

for (int nTry = 0; nTry < 3; nTry++)
{
  try
  {
     AttemptOperation();
     break;
  }
  catch (OperationFailedException){}
  await Task.Delay(2000);
}

The Task.Delay method produces the necessary task to await on, and thus you give up the thread. When the task enters the completed state, your loop can continue.

Task.WhenAll

Task.WhenAll creates a task that is deemed to have completed when all of a set of supplied tasks have completed. This allows you to await for a set of tasks to complete before proceeding, but without blocking the current thread. Examine the code in 7-14.

Listing 7-14.  Downloading documents one by one

public static async Task DownloadDocuments(params Uri[] downloads)
      
{
  var client = new WebClient();
  foreach (Uri uri in downloads)
  {
     string content = await client.DownloadStringTaskAsync(uri);
     UpdateUI(content);
  }
}

Calling this code on the UI thread would not result in the UI thread locking up, which is good. However it is not the most efficient way to handle downloads from multiple sources, since you will only request one download at a time. Ideally you would like to download the documents simultaneously.

Taking advantage of Task.WhenAll, you could create all the tasks upfront and then wait on a single task; that way you would download the documents in parallel. Listing 7-15 shows a possible implementation.

Listing 7-15.  Download many documents asynchronously

public static async Task DownloadDocuments(params Uri[] downloads)
{
 List<Task<string>> tasks = new List<Task<string>>();
 foreach (Uri uri in downloads)
 {
   var client = new WebClient();
 
   tasks.Add(client
             .DownloadStringTaskAsync(uri));
 }
 
 await Task.WhenAll(tasks);
  
 tasks.ForEach(t => UpdateUI(t.Result) );
}

The code in Listing 7-15 will produce the final result quicker but won’t update the UI until all the downloads are complete. Another alternative would be to spin off all the tasks, keeping each task in a collection. Then, provide a loop that awaits on each one in turn. This would possibly update the UI as items are completed, assuming they completed in the order they are initiated.

Task.WhenAll, Error Handling

While it would be nice to ignore failures, you obviously shouldn’t. The code in Listing 7-15 could possibly throw multiple exceptions as part of the downloading activity. As discussed earlier, if you just use a simple try/catch block, all you will get is the first exception of a possible set of exceptions contained inside the AggregateException. So while just simply calling await Task.WhenAll is convenient, it is often necessary to perform the statement as two steps: obtain the WhenAll task and then await on it (Listing 7-16).

Listing 7-16.  Observing WhenAll task exceptions

Task allDownloads = Task.WhenAll(tasks);
try
{
   await allDownloads;
   tasks.ForEach(t => UpdateUI(t.Result));
}
catch (Exception e)
{
   allDownloads.Exception.Handle(exception =>
   {
     Console.WriteLine(exception.Message);
     return true;
   });
}

Alternatively, if you care about knowing which Task was responsible for a given exception, then you will have to iterate through the tasks you supplied to WhenAll, asking each task in turn if it faulted, and if so, what are the underlying exceptions. Listing 7-17 shows such an example.

Listing 7-17.  Observing WhenAll specific task exceptions

catch (Exception e)
{
  for (int nTask = 0; nTask < tasks.Count; nTask++)
  {
    if (tasks[nTask].IsFaulted)
    {
       Console.WriteLine("Download {0} failed",downloads[nTask]);
                        tasks[nTask].Exception.Handle( exception =>
       {
          Console.WriteLine(" {0}",exception.Message);
          return true;
       });
    }
  }
}

Task.WhenAny

Task.WhenAny takes a collection of tasks, and returns a task that is deemed to have completed when any one of tasks completes. You can therefore solve the lack of immediate UI update issue in Listing 7-16 using the code in Listing 7-18.

Listing 7-18.  Download many documents and update UI they download

public static async Task DownloadDocumentsWhenAny(params Uri[] downloads)
{
  List<Task<string>> tasks = new List<Task<string>>();
  foreach (Uri uri in downloads)
  {
    var client = new WebClient();
 
    tasks.Add(client.DownloadStringTaskAsync(uri));
  }
  while (tasks.Count > 0)
  {
    Task<string> download =
                 await Task.WhenAny(tasks);
 
    UpdateUI(download.Result);
 
    int nDownloadCompleted = tasks.IndexOf(download);
    Console.WriteLine("Downloaded {0}",downloads[nDownloadCompleted]);
    tasks.RemoveAt(nDownloadCompleted);
  }
}

This now produces the desired result: UI updates as each asynchronous request completes. Notice that the await is not returning a string, but a Task<string>. This is a little strange as up to now you have seen await coercing the result of the task, and in fact it still does—it’s just that the Task.WhenAny wraps the Task<string> with a Task, making the Task.WhenAny return type a Task<Task<string>>. This is to allow the caller to be told what task has completed. If you just got the string, you would not know which task had completed.

So, all good? Well, not really. Why so? Well, you can see that each iteration of the loop calls two tasks:

Task.WhenAny, which registers a continuation for all tasks in the list that are not in a completed state.

Tasks.IndexOf(downloaded), which performs a linear search of the entire list, looking for a match.

For both these pieces of code, the execution cost increases as the number of tasks increases. It is further compounded by the fact that the cost is repeated after each subsequent task completes, albeit with a lesser and lesser cost. In effect, using this technique for 10 tasks will result in one task having a possibility of 10 defined continuations, as opposed to just one with Task.WhenAll.

So Task.WhenAny is at first glance the obvious choice for this kind of usage, but beware: it doesn’t scale. In Chapter 8 you will build a version that does scale. Use Task.WhenAny when you have a small number of tasks, or you only wish to take the result from the first completed tasks. For example, Listing 7-19 queries three web servers for a result and acts on the first response.

Listing 7-19.  First task to complete wins

public static async Task<string> GetGooglePage(string relativeUrl)
{
  string[] hosts = {"google.co.uk","google.com","www.google.com.sg" };
 
  List<Task<string>> tasks =
                (from host in hosts
                 let urlBuilder = new UriBuilder("http", host, 80, relativeUrl)
                 let webClient = new WebClient()
                 select webClient
                    .DownloadStringTaskAsync(urlBuilder.Uri)
                    .ContinueWith<string>(dt =>
                                {
                                  webClient.Dispose();
                                  return dt.Result;
                                })
                ).ToList();
  return await Task.WhenAny(tasks).Result;
}

Wait—not so fast. The code in Listing 7-19 will return the result of the first task to complete. What if it fails? You obviously would be happy to wait to see if one of the other servers could respond. A more reliable version would replace the await Task.WhenAny with the code in Listing 7-20.

Listing 7-20.  First task to run to completion wins

var errors = new List<Exception>();
do
{
   Task<string> completedTask = null;
              
   completedTask= await Task.WhenAny(tasks);
   if (completedTask.Status == TaskStatus.RanToCompletion)
   {
      return completedTask.Result;
   }
 
   tasks.Remove(completedTask);
   errors.Add(completedTask.Exception);
 
} while (tasks.Count > 0 );
 
throw new AggregateException(errors);

WhenAll and WhenAny are the built-in task combiners; they are very basic and we often need to add additional plumbing around them for use in our applications. In the next chapter you will develop more elaborate task combiners that will not require as much basic plumbing—for example, wait for all tasks to complete, but if any one fails cancel all outstanding tasks.

async/await Mechanics

Now that you have observed the magic, it is probably worth having a quick look at how this magic is achieved. In this final part of the chapter, you will examine how the compiler may rewrite your async method to achieve the continuations. To be very clear at the outset, we are not going to attempt to completely recreate everything, but just show you the bare bones so you get some idea of what is going on.

For this analysis, use the code in Listing 7-21.

Listing 7-21.  Asynchronous tick/tock

private static async void TickTockAsync()
{
   Console.WriteLine("Starting Clock");
   while (true)
   {
      Console.Write("Tick ");
 
      await Task.Delay(500);
 
      Console.WriteLine("Tock");
 
      await Task.Delay(500);
    }
}

From what you have seen and learned so far, you know that the TickTockAsync method, when called, will continue executing on the calling thread up to the point when Task.Delay returns, at which point the method returns to the caller. When the delay task completes it will then “move” onto the next piece of code, and so the pattern repeats itself for each of the await calls. You could consider each of these pieces of code as a series of separate states, as in Figure 7-1. Now it turns out compilers are good at building state machines; all that is required now is a standard way for it to detect when a state transition should happen.

9781430259206_Fig07-01.jpg

Figure 7-1. Asynchronous tick/tock state machine

It may be a reasonable assumption to assume that await only works with Tasks since they are the asynchronous building block for .NET 4.5, and the compiler could just utilize the Task.ContinueWith method. However, the compiler provides a more general approach. It turns out that the expression on the right side of the await keywords needs to yield an object that has a method on it called GetAwaiter. This method takes no parameters and can return any type it likes as long as this type.

  • Contains a boolean property called IsCompleted;
  • Contains a method called GetResult, takes no parameters, and the return type can be anything—this type will ultimately be the return type from the await statement; and
  • Implements the interface INotifyCompletion (see Listing 7-22).

    Listing 7-22.  INotifyCompletion interface

    namespace System.Runtime.CompilerServices
    {
        public interface INotifyCompletion
        {
            void OnCompleted(Action continuation);
        }
    }

It is therefore this awaiter object that provides the continuation functionality through the call to OnCompleted. The method is supplied with the delegate containing the code, which represents the next state. The code in Listing 7-23 shows a ridiculous awaiter implementation that represents something that never completes.

Listing 7-23.  A ridiculous awaiter implementation

public struct WaitingForEverAwaiter : INotifyCompletion
{
  public bool IsCompleted { get { return false; } }
  public void OnCompleted(Action continuation)
  {
     return;
  }
 
  public int GetResult()
  {
    return 0;
  }
}
 
public class WaitingForEverAwaiterSource
{
  public  WaitingForEverAwaiter GetAwaiter()
  {
     return default(WaitingForEverAwaiter);
  }
 }
 
. . .
private static async void WaitingForEverAsync()
{
    int result =  await new WaitingForEverAwaiterSource();
    // Never gets to here
 }

The Task and Task<T> classes simply implement a GetAwaiter method that returns an awaiter, which utilizes the Task to provide the necessary functionality required of the awaiter object. The awaiter object is therefore to some degree simply an adapter. What you will be able to glean from this is that this awaiter object has all the necessary parts to build the continuations between the various states.

Now you have the necessary building blocks to register for completions, which will cause the state changes in your state machine. You can now look to rewriting your async method along similar lines to what the compiler will do for your initial TickTockAsyncMethod() (see Listing 7-24).

Listing 7-24.  Asynchronous tick/tock as a state machine

private static void TickTockAsync()
{
   var stateMachine = new TickTockAsyncStateMachine();
   stateMachine.MoveNext();
}
 
public class TickTockAsyncStateMachine
    {
        private int state = 0;
        private TaskAwaiter awaiter;
 
        public void MoveNext()
        {
 
            switch (state)
            {
                case 0:
                    {
                        goto firstState;
                    }
                    break;
                case 1:
                    {
                        goto secondState;
                    }
                    break;
                case 2:
                    {
                        goto thirdState;
                    }
                    break;
            }
            firstState:
            Console.WriteLine("Starting clock");
            goto secondAndHalfState;
            secondState:
            awaiter.GetResult();
 
            secondAndHalfState:
            Console.Write("Tick");
            awaiter = Task.Delay(500).GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                state = 2;
                awaiter.OnCompleted(MoveNext);
                return;
            }
            thirdState:
            awaiter.GetResult();
            Console.WriteLine("Tock");
            if (!awaiter.IsCompleted)
            {
                state = 1;
                awaiter.OnCompleted(MoveNext);
                return;
            }
 
            goto secondState;
        }

The code in Listing 7-24 should be fairly self-explanatory. The original async method is replaced by creating an instance of a state machine and initially kicking it off via a call to MoveNext. The MoveNext method is in effect the body of the original async method, albeit interleaved with a switch/case block. The state field is used to know what code to run when MoveNext is called again. When each piece of code reaches the point of the original await operation, it needs to orchestrate the transition to the next state. If the awaiter object is already marked as completed, it just transitions immediately to the next state; otherwise it registers a continuation to call itself back and returns from the method. When the awaiter object deems the operation to have completed, the OnCompleted action is called and the next piece of code dictated by the state field is executed. The process repeats until the state machine is deemed to have completed. (Note in this case that should never happen unless an unhandled exception is thrown).

Summary

That concludes your initial look at async and await. In this chapter you have seen how these keywords provide an effective way of consuming and composing asynchronous methods while still keeping the structure of the code as simple as their synchronous counterparts. These keywords will become common in all of your asynchronous code, be it UI development or streamlining server-side asynchronous processing to ensure no thread is left blocked.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.219.130