Before we look at creating our own asynchronous methods, we should look at how to consume asynchronous methods. More specifically, we'll look at how to consume "awaitable" methods. What is an "awaitable" method? These are methods that return Task or Task<T> (and they can also return "void" and there are some other awaitables, but we'll just worry about Tasks today).
You can get the code download for this article on GitHub: https://github.com/jeremybytes/using-task. Each step is in a separate branch that I'll point out as we go along.
[Update: Articles in this series are collected here: Exploring Task, Await, and Asynchronous Methods. And if you want a video version of this article, look here: Consuming Asynchronous Methods]
The Starting Library
In our application, we have several projects set up. First, we have a WebAPI project that provides us some RESTful services. Next, we have a library project that contains a repository. This repository contains an awaitable method that we want to use (from the "UsingTask.Library" project):
We won't worry about the details of this method. Today, we're just looking at how we consume this method. One thing to note is that I added an artificial 3 second delay inside this method call. This way, we'll be able to tell that our code is actually running asynchronously.
There are a couple of things to note about the "Get" method. Most importantly, this returns a "Task<T>". This means that this is an awaitable method. Since the Task has a generic type parameter, we know that "List<Person>" is the actual data that we can get out of this method.
Don't worry about the "async" keyword here. As we'll see, "async" is important for what's inside the method, but what makes this particular method awaitable is that it returns a Task.
Using Task Directly
Before we dive into looking at the "await" operator, let's see how we would consume this method by using Task directly. This will give us a better idea of what happens when we use "await".
For this, we have a WPF application (in the "UsingTask.UI" project). Here's what our UI looks like (from "MainWindow.xaml"):
We have 2 buttons to fetch data (plus a clear button), and we have a list box to hold our output.
We'll start with the event handler for the "Fetch Data (with Task)" button. This is in the code-behind for the main window.
We have a class-level "repository" field which is of type "PersonRepository". So, we'll start by calling the "Get" method on our repository. As we saw above, this method returns "Task<List<Person>>". And if we hover over the "var" keyword, we see just that:
Let's be a little more explicit with our variable type so that our code is a bit easier to read:
This gives us a Task that we can work with. Now a Task is an asynchronous operation. We can think of this as a promise -- at some point in the future, we will get a "List<Person>". But that also means we may need to wait a while before that promise is fulfilled. When we call "Get", it returns immediately. It does *not* wait for the asynchronous operation to complete.
But what if we want to do something once the Task is complete? This is where we can use the "ContinueWith" method on Task. This will run code once the current Task is done with its processing.
Let's look at what kind of parameter we need for "ContinueWith". If it's not already showing, you can use Ctrl+Shift+Space to show the method signature(s).
This shows us that we need an "Action<Task<List<Person>>>". Huh?
Well "Action" is simply a built-in delegate type. For more information, you can check out one of my recent videos: C# Delegates: Action<> and Multicast Delegates. The short version is that we need a method that has "Task<List<Person>>" as a parameter and returns void.
So, we'll create that method and pass it in as a parameter to "ContinueWith".
Here we've created a new method called "FillListBox" that takes a "Task<List<Person>>" as a parameter.
The first thing we do is look at the "Result" property on our Task. The "Result" will be the "List<Person>" that we're looking for. Since we are in the "ContinueWith" method here, we know that the initial Task operation has completed, so "Result" should have a valid value (barring any exceptions, of course).
This means that our "people" variable is populated with the "List<Person>" coming back from our library. Once we have this, we can loop through the items to populate our list box.
So, let's run our application and try things out.
Uh oh. It looks like we have a bit of a problem. We're trying to populate our "PersonListBox" which we can only do from the UI thread, but apparently we're on a different thread.
With traditional threading, this is where we would use "Dispatcher.Invoke" to get things back on the UI thread. But we have a bit of an easier way of doing things here.
When we call "ContinueWith", we can give it another parameter:
This "TaskScheduler.FromCurrentSynchronizationContext()" tells our "ContinueWith" method that we want to run this "FillListBox" method on the same thread that we're calling it from (which is our UI thread since we're calling this from our button click event handler).
With this in place, our application runs as expected:
And since there is a 3 second delay in the library code, we'll have to wait a little bit after we click the "Fetch" button. But if we try to interact with the UI (for example, by moving the window on the screen), we'll see that the UI is still responsive. So we're not locking up the UI thread during this operation.
This code can be found in the "01-Task" branch: https://github.com/jeremybytes/using-task/tree/01-Task.
Using Task with a Lambda Expression
Now, I really love lambda expressions, so whenever I see "Action" as a method parameter, I want to use a lambda.
By using a lambda expression, we can inline our "FillListBox" method:
This eliminates our separate "FillListBox" method; it is replaced by a lambda expression in the "ContinueWith" parameter. You can check out Learn to Love Lambdas or C# Delegates: Get Func<>-y (+ Lambdas) for details on how to change the method call into a lambda expression.
I like this because it puts all of the code into one place (the event handler) instead of spreading it across two methods. In addition, we can use captured variables and other cool features of lambdas. (Have you figured out that I really love lambdas?)
This code can be found in the 02-TaskLambda branch: https://github.com/jeremybytes/using-task/tree/02-TaskLambda.
And even though this isn't all that difficult. There's a much easier way to accomplish this exact same task -- by using "await".
Using "await" with an Awaitable Method
Now we'll look at our second button. We'll do the same action (getting data from our repository and loading our list box), but we'll "await" things rather than deal with the Task directly.
If we look at our "repository.Get()" method, we'll see that it is listed as "awaitable", and we even get a hint on how we can use the method with the "await" operator:
This shows us that if we "await" the "Get" method, then the result will be a "List<Person>" (rather than the "Task<List<Person>>" that "Get" returns). So, "await" will wait for the Task to finish and then give us the "Result" from the "Task".
So, in our second button's event handler, let's use the "await" operator like the hint tells us:
This isn't quite what we expected. We've got red squigglies which means something is wrong. If we hover over the squigglies, it will tell us that we need the "async" keyword on the method.
Notice that once we add the "async" keyword to the method, the "await" operator is lit up (and the squigglies are gone). This is due to the way that "await" is parsed by the compiler.
"async" and "await" Go Together
The "await" operator was added in Visual Studio 2012 to leverage asynchronous support in .NET 4.5 (and Windows Runtime). The problem is that since "await" was not a keyword before, it can't magically become one since it could break existing code that is using "await" as a variable name or something else like that.
So instead, "await" is a contextual operator. The way the compiler tells the difference is whether the method is marked as "async". If the method is marked "async", then the compiler assumes that any uses of "await" refer to the context that we have here. Otherwise, it is treated just like any other identifier.
This means that "async" does not magically make a method asynchronous. All it does is tell the compiler that if it finds "await"(s) in the method, it needs to treat them with the special context that we want here.
With this in place, we can finish up our method:
Since we get a "List<Person>" by awaiting our method, we can simply continue on with our code -- looping through our person collection and populating the list box. There's no need to create a separate "ContinueWith" or make sure that we're on the right thread. The compiler handles all of this for us.
When we run our application, we'll see the same results as when we used Task directly:
During the 3 second delay, our UI is still responsive.
This code can be found in the 03-Await branch: https://github.com/jeremybytes/using-task/tree/03-Await.
So which would you rather write? This:
It's much easier for us to use "await". Now we only have 1 method (instead of splitting into 2 methods (even if one of those methods is a nested lambda expression)). And we don't have to worry about marshalling back to the UI thread. The synchronization context is handled for us automatically.
Even though our method with "await" is easy for us to read and write, the compiler turns it into something a bit more complex. It looks more like our manual task with the continuation (plus some other stuff). But "await" makes it very easy to consume methods while maintaining "normal"-looking code.
Limitations with Async
There are a few limitations when we use the "async" keyword. The main limitation is with our return type. When we mark a method as "async", we must have one of three return types:
If we have a Task, then we can check the status, get notified of faults, and look for exceptions. We'll look at all of this in an upcoming article.
A Console Application
Let's see what happens when we run into one of these limitations. For this, we'll try to use our same library in a console application. Here's our starting point (in the "UsingTasks.Tester" project):
So let's add our awaitable method call:
This shows the error around the "await" operator like we saw earlier. This means that we need to add "async" to our method:
But we still have a problem. "Main" isn't happy. Here's the error message:
Now what? We're not allowed to use "async" with the "Main" method in our console application.
But all is not lost. We can still get some benefit out of this library. Remember in our "FillListBox" method where we checked the "Result" property on the Task?
We can do that in our console application:
Since "Get" returns a Task, we can directly check the "Result" property. When we do this, we get the output that we expect (mostly):
This code can be found in the 04-Console branch: https://github.com/jeremybytes/using-task/tree/04-Console.
Danger Danger Danger
Now why did I say "mostly"? It's because we broke the asynchrony here. When we check the "Result" property, our thread will wait for the "Result" to be populated. This means that our application is hung during this operation. So when we have the 3 second delay in our library, our application will stop responding.
Our other option would be to use the "ContinueWith" method on the Task like we did above. And this is generally what we want to do. When we use "Result", we need to be aware that it could lock up our application.
Using ContinueWith in the Console Application
[Update 12/28/2014: I decided to add the code for using "ContinueWith" in the console application. This code can be found in the 05-ConsoleContinue branch: https://github.com/jeremybytes/using-task/tree/05-ConsoleContinue.]
If we don't want our console application to lock up during the 3 second delay, we can add a continuation to the Task like we did with our WPF application. Here's our updated Main method:
Instead of using the "Result" immediately, we get the Task from our "Get" method (just like above). Then we use the "ContinueWith" method to put in the code that we want to run when the task is complete.
In the "FillConsole" method, we get the "Result" from our Task (which is now complete, so we're not locking up any threads), and then we loop through the list and output to the console. If we wanted, we could convert this to a lambda expression like we did above, but I'll leave that as an exercise for the reader.
Notice that we do not have "TaskSchedule.WithCurrentSynchronizationContext" like we had in our WPF application. That's because we don't need it here. We can write to the Console from whatever thread we like, so there is no need to get back to the main thread. (If we did need to run this code on the main thread for some reason, that's where things get a bit interesting. But we won't worry about that today.)
Just to show that we are not locking up our thread, I added the "for" loop to output the numbers 0 through 4 to our Console. If we run the application, we'll see that these numbers output immediately (they don't wait for the Task to complete). Then after 3 seconds, we get our normal output:
This shows us that we are getting the asynchronous behavior that we expect.
So this was a whirlwind tour of consuming an awaitable method. We saw how we could use the Task directly and manually create a continuation. Or we can use the "await" keyword and let the compiler generate that continuation for us. By seeing how to do thing manually, we get a better appreciation for the automation that the compiler gives us.
We'll look at related topics in upcoming articles, including error handling, creating our own awaitable methods, and looking at situations where our methods may sometimes run synchronously even if they are awaitable.
It takes a bit of time to wrap your head around this topic, so feel free to send me any questions on what I've covered here (as well as topics you'd like to see covered in future articles). We'll work in small steps to get a good understanding of what goes on in the world of asynchronous programming.