Asynchronous programming is awesome, and C# makes it very easy. At a high level, asynchronous programming is all about not letting independent tasks block each other so you can do more than one thing at a time. One common analogy used to describe the asynchronous pattern is cooking. Let’s say I am a chef in a restaurant, and a breakfast order comes in for eggs, toast, and an apple. If I were to prepare this meal synchronously, I would crack an egg on the griddle, wait for it to cook, and put it on the plate. Then I would put bread in the toaster, wait for it to pop up, and put the toast on the plate. Finally, I would get the apple from the refrigerator, and put it on the plate to complete the order. But this is clearly an inefficient way to get the work done, and in the real world we would never do this – there’s no need to wait for the eggs to complete before starting the toast and vice-versa. It makes more sense to prepare the meal asynchronously. To more efficiently do all the work required to prepare the meal, I would crack the egg on the griddle, then put the bread in the toaster. While I’m waiting for those two long running tasks to finish, I can get the apple from the fridge and put it on the plate since the execution of that task is not blocked or prevented by making eggs or toast. Now I can just wait until both the eggs and toast are done, and I can complete the order. I’ve created a significant gain in efficiency by letting two independent tasks run at the same time, and doing the comparatively shorter task(s) while waiting for both to complete.
Let’s take this example of waiting for long running tasks to complete into some code. Suppose your application needs to call three methods to get the data required for constructing a new object. Following the same synchronous approach as the first breakfast order, we call each of the three methods one after the other, and use the data returned to construct the new SampleData object. Here’s a quick example:
There is nothing inherently wrong with this code, but what if one or more of those methods take a significant amount of time to execute? Suppose Foo() makes an HTTP request to an API that is responding slowly, Bar() is application code that performs a lengthy calculation or database query, and Baz() is something that executes very quickly. Since none of these three methods are dependent on the return values of each other, we can gain significant performance by running them all at once, and then waiting for the potentially longer operations to complete before creating our SampleData object – this is asynchronous programming.
There are many, many documents, articles, and blog posts out there that do an excellent job of dissecting and explaining asynchronous programming with C# in detail, so I’m not going to rehash that same information here. One of my favorites is Async and Await by Stephen Cleary. I would also recommend Asynchronous programming with async and await (C#) from Microsoft for a more in-depth look at what happens behind the scenes with asynchronous method execution. And without trying to further complicate the issue, I think it’s important to point out that asynchronous programming is not the same thing as multi-threaded programming in C# – see: What is the difference between asynchronous programming and multithreading?
This post is for people like myself – programmers who learn better from jumping right into code and stepping through each line to see how a program executes. When I’m first exploring an unfamiliar concept, reading documents and looking at sample code snippets can sometimes be a bit confusing or overwhelming. In many cases, I learn best when I can see a concept in a working, concrete form. I’ve created a small .NET 4.7.1 console application that will walk you through asynchronous programming with C# using a complete and practical example. The repository is accessible here:
Let’s get started! Clone the repository, and open up the solution. You will see two source code files inside, Program.cs and SampleDataLayer.cs.
Let’s look at the Main() method in Program.cs – this is our application entry point. You will notice that the method declaration of Main() includes the “async” keyword. This is because inside Main() we will be calling and awaiting on some asynchronous methods. Always remember that the “async” keyword simply enables the use of the “await” keyword inside a method. Inside Main(), we are going to run a small experiment by calling two methods: RunDemoSynchronous() and RunDemoAsync(). The methods both contain nearly the same code, but RunDemoSynchronous() will execute synchronously and RunDemoAsync() will execute asynchronously. We will compare the performance of each. Here is the entire Main() method:
To begin, let’s run the application and see the output:
Inside Main(), we create a Stopwatch object and use it to capture the elapsed execution time when calling RunDemoSynchronous() and RunDemoAsync(). We can see from the console output that RunDemoAsync() ran about 3 seconds quicker than RunDemoSynchronous(). So what’s happening when each of these methods are called?
The first half of the experiment starts by calling RunDemoSynchronous() and a message is logged to the console to indicate we have begun execution of this method (“RunDemoSynchronous() start.”). Inside, three methods are called to fetch the data necessary to construct our SampleData object:
GetDelayedApiResponse() calls an API that waits for a specified number of seconds before responding to simulate a lengthy API request. Here is the GetDelayedApiResponse() method as it is written in SampleDataLayer.cs:
The program also outputs a message to the console here when we enter the method, and again after the WebClient received the response. The app will output similar console messages throughout so we can see the current status of the program as it is executing – this is just some good old fashioned printf() debugging.
After exiting GetDelayedApiResponse(), we move on to the next line in RunDemoSynchronous(), calling SimulateLongProcess(). This method simulates long running application logic in our code by simply pausing for the specified number of seconds. This method could represent a CPU-intensive calculation, a database query, or any other lengthy process in your code.
The last method we call before we can construct or SampleData object is Foo(). This method simulates short running work in application code, and will return almost immediately.
Back inside RunDemoSynchronous(), we have now called three methods and stored their return data in three variables. We can now create our SampleData object, exit the method, and stop the currently running Stopwatch.
The three methods we just called, GetDelayedApiResponse(), SimulateLongProcess(), and Foo(), were blocking because they were called synchronously, one after the other. We can verify this by looking at the console output:
GetDelayedApiResponse() starts and completes, then SimulateLongProcess() starts and completes, and finally Foo() starts and completes. RunDemoSynchronous() took about 7 seconds because we set the delay time for GetDelayedApiResponse() and SimulateLongProcess() to 4 and 3 seconds, respectively. The execution time of Foo() is comparatively much quicker, adding only a fraction of a second to the total execution time.
We can achieve a significant performance gain by implementing an asynchronous pattern and running those methods all at once, waiting for each to complete before we construct the SampleData object. GetDelayedApiResponse() and SimulateLongProcess() do not need to know about each others’ output, so we can execute both at the same time. While we wait for the output of those two methods, we can call Foo().
Back in the Main() method, the second part of the experiment is run by calling RunDemoAsync(). This method will demonstrate asynchronously executing the exact same workflow as we did above. As a result, we will see a significant performance gain. First, we reset the stopwatch and call RunDemoAsync(). Notice the change in syntax when calling this method:
RunDemoAsync() is a an async method, and we need to wait for it to complete before stopping our stopwatch by using the await keyword. Once we’re inside RunDemoAsync(), we get variables dataA, dataB, and dataC in a similar fashion as we did above, but we are now calling GetDelayedApiResponseAsync() and SimulateLongProcessAsync() instead. These are asynchronous versions of GetDelayedApiResponse() and SimulateLongProcess(). Async methods usually return Task, which is just an asynchronous operation that can return a value. (If the async method returns null, Task is returned).
Let’s take a closer look again at the console output and see how the execution flow differs inside RunDemoAsync().
Notice this time that GetDelayedApiResponseAsync() is started, but instead of the next console line reading “GetDelayedApiResponseAsync() complete” as we saw in the first example, we see “SimulateLongProcessAsync() start” and “Foo() start” on the next lines. From this output, we can see that the next two lines of code were called without waiting for the first line to finish executing. Let’s take a look at GetDelayedApiResponseAsync():
Notice that the method return value is now a Task, and the method declaration also includes the “async” keyword. Remember that using the async keyword enables the use of the “await” keyword in a method. We’re using the WebClient class to make the request to the API. GetDelayedApiResponse() used the WebClient.DownloadString() method to make the API request, but in GetDelayedApiResponseAsync(), we will use WebClient.DownloadStringTaskAsync() instead. Because we made our method async, we can await on DownloadStringTaskAsync(). The program will execute up until this line, and while awaiting for DownloadStringTaskAsync() to come back with our API response, the execution flow will return to the next line in RunDemoSynchronous().
SimulateLongProcessAsync() is written in a similar fashion – the return type is now a Task, and we’ve added the async keyword to the method declaration:
We are just pausing execution of the program here as we did in SimulateLongProcess(), only this time we will call an async method inside, awaiting on Task.Delay() rather than calling Thread.Sleep(). Once again, making our method async simply means enabling the use of the “await” keyword within. Just like GetDelayedApiResponseAsync(), once we reach this line:
The execution flow of the program returns to the method that called our async method. Now that we have called those two async methods, we’re back at this line in RunDemoAsync():
Foo() remains unchanged in the second part of our experiment – it is still a synchronous method and we still call it synchronously. We know that Foo() gets called and executes very quickly, so we will need to wait for the other two methods to return before we can create our object:
As soon as the program is done awaiting dataA and dataB, the SampleData object will be constructed, and the Console will output the “SampleData object ‘sample’ created” message along with the values of each property in the object. Method RunDemoAsynchronous() will exit, and we return to the Main() method where the final messages indicating that RunDemoAsynchronous() is complete are written to the console. The elapsed time to complete the second half of the experiment is just over 4 seconds – a little bit more than a 3 second improvement in total execution time over the RunDemoSynchronous() method!
This represents quite a significant improvement in the application’s performance. By implementing an asynchronous pattern in the program, we get where we need to be more quickly than we did before, and the benefit is easy to see. This is just one example of asynchronous programming, and it only scratches the surface. There are many ways to implement the asynchronous programming pattern and I hope this post serves as a jumping off point for others to leverage similar patterns in their C# applications.