The Down and Dirty Guide: Async and Await in C#

I will share a little secret with you; I’ve found asynchronous programming a hard, but necessary, competency to master. In fact, it hurts my head. Yet, being able to do asynchronous programming effectively is particularly important now when so much data comes from data stores that are unpredictable in terms of response time. So, any help I can get simplifying the amount of stuff I need to know to do my programming work is always welcome. When it comes to asynchronous programming, .NET has made my life a lot easier by adding the class Task and the keywords async and await to my C# programming toolkit.

Task, async, and await have been around for a while. Nonetheless, I can imagine that there are few of you out there who might have hurting heads on this topic. Hopefully, this piece will give you the basics you can use to get some relief.

So, let’s get down and dirty.

Understanding a .NET Task

The most important thing to understand about the keywords async and await is the keywords’ relationship to a .NET Task. Think of a Task as an object that has work executing asynchronously on a particular core of your multi-core computer. .NET and C# in their collective, infinite wisdom have made it so that when you use a Task, all the work that is done in terms of core assignment and thread utilization for that Task is done automagically. The benefit is apparent. You can do a lot of effective, concurrent multithreading easily without having to bear the burden of the thread management you’d do otherwise.

Working with async and await

When you define your method using the keyword async, .NET expects two things. First, that the method on which you declare, async, returns a Task<T> and second, that internally within that method, you declare some asynchronous behavior with the keyword await. Please look at Listing 1.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Reselbob.DownAndDirty
{
   public class AsyncDemo
   {
      public async Task<bool> DoTimeIntensiveWork()
      {
         await Task.Run(() =>
         {
            Console.WriteLine(@"The threadId for the Task
               running in AsyncDemo.DoTimeIntensiveWork()
               is: {0}",
               Thread.CurrentThread.ManagedThreadId);
               var time = DateTime.Now.ToString("hh:mm:ss:fff");
            Console.WriteLine(@"Start time for the Task in
               AsyncDemo.DoTimeIntensiveWork(): {0}", time);

            //put the task to sleep to emulate time working
            Thread.Sleep(500);

            Console.WriteLine("--- behavior that takes 1/2
               second ---");
            time = DateTime.Now.ToString("hh:mm:ss:fff");
            Console.WriteLine(@"The Task in
               AsyncDemo.DoTimeIntensiveWork() finished at: {0}",
               time);
         });

         return true;
      }
   }
}

Listing 1: A very simple method that uses the keywords async and await

Notice that the method AsyncDemo.DoTimeIntensiveWork() in Listing 1 calls Task.Run(...) which has a lambda expression passed as a parameter. This lambda expression has some timestamp, Console.WriteLine() behaviors, and Thread.Sleep() behavior that are executed. Also, notice that the keyword await declared before the Task.Run(....) invocation. Conceptually, the keyword await makes calling code wait for Task.Run(...) to execute. Thus, the method code does not “run over” Task.Run(...) to the statement return true;. Rather, it waits. Were DoTimeIntensiveWork() declared as a typical method, not using the keyword async and absent of keyword await at the Task.Run(...) line, program control would call Task.Run(...) and keep going to the statement, return true. The code within Task.Run(..) would go on its merry way, asynchronously, executing in its own thread. However, the keywords async and await are in play. So, DoTimeIntensiveWork() waits for the asynchronous behavior of the Task to complete and then, once completed, the method returns a value of true. Well… no, not really. Returning true is a red herring. Remember, the keyword await has been used. Thus, .NET requires that DoTimeIntensiveWork() returns a Task<T>, in this case, Task<bool>. The return value, true, declared in the method DoTimeIntensiveWork() is really the value of the Result property of the Task returned by the method.

I know, there is a lot of implicit hocus pocus going on. The important thing to remember is that when you use the keyword async when declaring a method, you will return a Task<T> in which T is the return type of the value returned internally in the method. The implied return value is accessed via Task.Result.

Note: Task<T> is an example of a C# Generic. If you need to brush up on Generics in C#, you can read a good MSDN article here.

The following items illustrate some examples of the implicit relationship between the Task<T> generic and the returned Task.Result, when using the the keyword async in a method declaration.

  • public async Task<string> DoWork(){ return "foo";}, the value of Task.Result is "foo"
  • public async Task<int> DoWork(){ return 5;}, the value of Task.Result is 5
  • public async Task<string[]> DoWork(){return {"moe","larry","curly"};}, the value of Task.Result is {"moe","larry","curly"}

So now the question becomes, “How can we work with methods that use the keywords async and await?”

Working with a Task

Listing 2 shows a unit test, CanRunAsyncDemoTest(). I am using the test, CanRunAsyncDemoTest(), as a code runner. So, don’t expect any Assert statements. The purpose of CanRunAsyncDemoTest() is to show you how to work with the Task returned from the method, AsyncDemo.DoTimeIntensiveWork() that we defined in the code in Listing 1.

CanRunAsyncDemoTest() writes console output that reports the identifier of the thread in which the test is running. Also, the code creates an object, demo of type AsyncDemo. As you see in Listing 1, AsyncDemo contains the async method DoTimeIntensiveWork().

Please pay close attention in Listing 2 to the line, var task = demo.DoTimeIntensiveWork(). demo.DoTimeIntensiveWork() returns a Task<bool>. Notice that the variable task has a method, task.ContinueWith(...).

/// <summary>
/// I am using the test as a code runner.
/// No Assert statements are included.
/// </summary>
[TestMethod]
public void CanRunAsyncDemoTest()
{
   var demo = new AsyncDemo();
   Console.WriteLine(@"The threadId for
      BasisTests.CanRunAsyncDemoTest()
      is: {0}\n---\n",
      Thread.CurrentThread.ManagedThreadId);
   Console.WriteLine("");
   var task = demo.DoTimeIntensiveWork();
   task.ContinueWith((t) =>
      {
         Console.WriteLine(@"\n---\ntask.ContinueWith()
            returns the value: {0}, at: {1}",
            t.Result, DateTime.Now.ToString("hh:mm:ss:fff"));
      });
   Thread.Sleep(1000);
}
 

Listing 2: Working with a Task returned from the async method DoTimeIntensiveWork()

ContinueWith() is a method that means, “Once the Task completes, do the behavior defined herein.”

Thus, demo.DoTimeIntensiveWork() returns a Task<bool>. When the returned task finishes executing its internal code, ContinueWith(...) takes over. The code in ContinueWith(...) outputs a string to the console that reports the time ContinueWith(...) executes.

The last line of the test method, CanRunAsyncDemoTest(), has a Thread.Sleep(1000) statement. Why?

The reason that I put the Thread.Sleep(1000) statement at the end of the methods is to give the code in the Task being executed within demo.DoTimeIntensiveWork() time to run. The Task internal to demo.DoTimeIntensiveWork() takes 500 milliseconds to run. If I did not put the 1000 millisecond Thread.Sleep in the code runner, CanRunAsyncDemoTest(), the code runner would terminate well before the 500 milliseconds that the Task in demo.DoTimeIntensiveWork() needs to complete its work. I’ve provided a diagram in Figure 1 that explains the control flow between the Tasks in play. Still, I can imagine things to be a bit confusing. You can download this code that has been provided with this article (available at the end of this article) and run it with some break points to get some run time experience that might help clarify matters.

Down
Figure 1: The Asynchronous flow between Task threads

As you can see, there is no blocking in the test function, CanRunAsyncDemoTest(). Rather, the async code runs on its own. task.ContinueWith() provides the way for the calling code to interact with the results of AsyncDemo.CanRunAsyncDemoTest(). Again, the asynchronous nature of it all is a tricky concept to grasp. So, don’t get frustrated if you don’t get it all once. It took me a while to have the light bulb go off.

Putting It All Together

Listing 3 shows the Console output from the code above in in Listings 1 and 2.

The threadId for BasisTests.CanRunAsyncDemoTest() is: 22
---

The threadId for the Task running in
   AsyncDemo.DoTimeIntensiveWork() is: 21

Start time for the Task in AsyncDemo.DoTimeIntensiveWork():
   10:12:49:949
--- behavior that takes 1/2 second ---
The Task in AsyncDemo.DoTimeIntensiveWork() finished at:
   10:12:50:450

---
task.ContinueWith() returns the value: True, at: 10:12:50:451

Listing 3: The Console output that is the result of CanRunAsyncDemoTest()

The key takeaways are these:

  • Notice that CanRunAsyncDemoTest() and the Task created internally in demo.DoTimeIntensiveWork() are running in two different threads. This makes sense, given the automagical nature of Task.
  • Notice that task.ContinueWith(...) starts almost immediately when demo.DoTimeIntensiveWork() ends. Remember that AsyncDemo.DoTimeIntensiveWork() will return a Task which, in this case, is the variable task. There is a keyword, await, on the Task internal to AsyncDemo.DoTimeIntensiveWork(). Using await means that AsyncDemo.DoTimeIntensiveWork() will not return its Task<bool> until the asynchronous behavior in AsyncDemo.DoTimeIntensiveWork() completes.

As I said in the beginning of this article, doing asynchronous programming hurts my head. And, I can see how the example I’ve put in play above can be a bit confusing. You’ll do well to do a few reviews of the example code provided. There is a lot going on in terms of code concurrency. But, believe me, the power that Task, async, and await brings to the table is considerable, just in terms of thread management alone. Once you get the hang of things, your programming chops will go up a notch. And, you’ll get all the credit while .NET does all the work.

Further Reading and Viewing

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read