With the multifold increase in processor performance as well as number of processor cores in a system, software systems are expected to scale horizontally with system hardware. However, software systems have the tough job of working with disparate systems that work at their own speeds (e.g. Network latency, Disk latencies, peripheral device latency and so on). So if we are building a system that does things synchronously, our throughput is always going to be limited by the slowest system in the chain.
Thus, the ability of our software to initiate a task and do something else till that task completes, goes a long way to ensure we don’t ‘bottleneck’ or ‘wait doing nothing’ on a slowly running component.
Rowan Miller had an excellent analogy of Async tasks to waiters at a restaurant, in his TechEd NA (2013) talk. To paraphrase – A system working asynchronously is like a waiter at a restaurant. More often than not, a waiter serving a table will be at Table 1, take an order, explain a menu item, deliver an order and then move away, free to do the same at Table 2. When Table 1 is done (deciding the order, requiring a refill, requesting the check) they will draw the waiter’s attention and the waiter would come back to Table 1 as soon as they become available (or immediately if they are available). This mechanism of one waiter serving multiple tables ensures that you don’t need as many waiters as the number of tables, to maintain optimal performance (in case of restaurant – experience).
If we consider a computation unit (CPU + Coprocessors + Cache + System Bus etc.) to be a waiter serving customers, then async operations is the way to make sure that they don’t waste time waiting while the customer decides what to order.
Enough analogies, let’s see some real code.
This article is published from the DotNetCurry .NET Magazine – A Free High Quality Digital Magazine for .NET professionals published once every two months. Subscribe to this eMagazine for Free and get access to hundreds of free tutorials from experts
The Sample MVC Application
Let’s say we have an application whose home page is split into four parts showing Emails, Tasks, Bookmarks and Notes. Ideally these would probably come from different feeds from disparate systems, but to keep things simple, we’ll get them from four tables in a database. We’ve filled up the tables with some sample data that I generated.
This demo uses Visual Studio 2013 Preview, but you can use VS 2012 as well. For the performance testing pieces, you’ll need the Ultimate SKU, or you can use the free WCAT tool as well.
Since I want to focus on the performance bit, I will not do a step-by-step walkthrough of how to build the application. You can directly get the code and try it out. However this is how the application is laid out:
The Web API services App
The Web API services app is using the latest Web API binaries. If you are using Visual Studio 2012, you can create a Web API application and upgrade to Web API preview version using the –pre parameter in the Package Manager Console. If you are using Visual Studio 2013 Preview, you are already setup with the latest preview binaries for Web API 2.0.
Once the project is setup, we use the following command to install the latest version of Entity Framework (which is version 6 beta at the time of writing).
PM> install-package EntityFramework –pre
Next we add the four entities that will serve as our data source – Email, Task, Bookmark and Note, each with the following properties:
public class Email
{
public int Id { get; set; }
public string Mail { get; set; }
public string From { get; set; }
public string To { get; set; }
public string Title { get; set; }
}
public class MyTask
{
public int Id { get; set; }
public string Starts { get; set; }
public string Ends { get; set; }
public string Title { get; set; }
public string Details { get; set; }
}
public class Bookmark
{
public int Id { get; set; }
public string Url { get; set; }
public string Title { get; set; }
}
public class Note
{
public int Id { get; set; }
public string Content { get; set; }
}
Finally we scaffold Entity Framework based Web API Controllers for each of the above entities, giving us the APIs required to do CRUD operations for these entities.
Our solution structure looks like the following
The Get APIs for each controller is as follows:
public IEnumerable GetBookmark()
{
return db.Bookmarks.AsEnumerable();
}
public IEnumerable GetEmail()
{
return db.Emails.AsEnumerable();
}
public IEnumerable GetNotes()
{
return db.Notes.AsEnumerable();
}
public IEnumerable GetTask()
{
return db.Tasks.AsEnumerable();
}
Instead of entering 100 rows of data manually, I used the GenerateData.com site to generate INSERT queries for the dummy data. They are included in the source code of the app.
The MVC Client App
In the same solution, we can add an ASP.NET MVC Client app that will use these web services. Now using Web Service in ASP.NET MVC app is trivial if you use AJAX to query service directly. But that would require CORS and other concerns like authentication etc. To keep things simple for us, we will instantiate a WebClient object and query each Web Service directly. The Client app will use its own proxy entities and return one Model object that we will bind in our MVC View.
Now this may not be the most elegant way to consume Web Services, but it will help us identify performance changes as we go from synchronous to asynchronous implementation.
Overall we end up with the following classes in our Client App
The DashboardController collates all the data from the WebAPI in the Index GET action method and returns an instance of a Dashboard Model to the Index view.
The Synchronous Application
The synchronous version of the Dashboard controller is as follows:
public ActionResult Index()
{
Stopwatch timer = Stopwatch.StartNew();
timer.Start();
var feeds = GetFeeds();
timer.Stop();
feeds.TimeTaken = timer.ElapsedMilliseconds;
return View(feeds);
}
public Dashboard GetFeeds()
{
string emails = new
WebClient().DownloadString("http://localhost:18545/api/Emails");
string myTasks = new
WebClient().DownloadString("http://localhost:18545/api/Tasks");
string notes = new
WebClient().DownloadString("http://localhost:18545/api/Notes");
string bookmarks = new
WebClient().DownloadString("http://localhost:18545/api/Bookmarks");
Dashboard dash = new Dashboard();
dash.Emails = Deserialize(emails);
dash.Bookmarks = Deserialize(bookmarks);
dash.Notes = Deserialize(notes);
dash.Tasks = Deserialize(myTasks);
return dash;
}
The Index.cshtml for the Dashboard is marked up such that it splits the screen into 4 parts each showing the Email, Bookmarks, Notes and Tasks. We also have a stopwatch that keeps track of how much time it takes to fetch the data on the page.
Setting up a Performance Test Project
To positively quantify the change in performance characteristics, a simple timer like we have above is often misleading because going async almost never gives you an improved page load time on the client. However what it does give you is better page response on higher load. The only way to load test is use a load testing tool. Today we’ll use a Performance Test project in Visual Studio and setup a load of concurrent users. The key characteristics to watch out for are the Average Page Time and the Count of page hits while the test was running. Lets setup the project first.
Step 1: Add a new Project and select the Test project template “Web Performance and Load Test Project”.
This will add a WebTest to the project automatically.
Step 2: Add a new Load Test to the project.
This will launch a new Load Test wizard.
Click “Next” to setup the Scenario, you can keep the default here
The “Next” page sets up the Load pattern. You can use the following settings but I had to tune it down to 10 maximum users, Start User count of 1 and Step User Count of 1. This completely depends on how far you can push your host system based on its hardware configuration. I hit the CPU and Memory thresholds pretty quickly as you will see in a bit.
Once the load is setup, next step is to setup the Test Mix Model. Here we’ve to add the WebTest1.webtest to the mix.
You can keep the defaults for the Network Mix, Browser Mix and Counter Sets. For the Run Settings, change the defaults to the following:
Load Test Duration, Warm-up duration: 20 seconds
Run Duration: 1 minute
Sampling Rate: 5 seconds
Click Finish to finish setting up the Load Test.
Step 3: Double click on the LoadTest1.loadtest file to open the Test configuration.
Step 4: Hit Ctrl+F5 to run the application without the Debugger (running tests with Debugger gives far worse results so start without Debugger).
Once IIS Express starts, start TaskManager and wait for the CPU spike to settle down.
Step 5: Now click on the ‘Run Load Test’ button to start the load test. Remember our code is synchronous at the moment. Once the test completes, you’ll get a test Summary as follows
The important figures as I mentioned earlier are highlighted above. As we can see, Average Page Time is 3.23 seconds and we were able to serve up 79 pages in the 1 minute that the test ran (with progressively increasing loads from 1 user to 10 users).
This gives us a baseline. Now lets’ convert our entire call stack to Asynchronous code and run the load test again to see what kind of performance we gain.
Converting to an Asynchronous Implementation
We will convert our Web API Services and EF DB calls to async first. EF6 has provided Async counterparts to all calls that return Data (not the Query). The updated Api Controllers are as follows.
public async Task> GetNotes()
{
return await db.Notes.ToListAsync();
}
public async Task> GetTask()
{
return await db.Tasks.ToListAsync();
}
public async Task> GetBookmark()
{
return await db.Bookmarks.ToListAsync();
}
public async Task> GetEmail()
{
return await db.Emails.ToListAsync();
}
As we can see, we are awaiting the ToListAsync calls and we have annotated our Controller methods as async and returning a Task of the Enumerable instead of the Enumerable itself.
This converts the service calls into async but what about the client? It is still calling the service synchronously.
Converting the MVC Client to call the services asynchronously
We switch to our AsyncMvcClient project and get to the DashboardController. We first update the GetFeeds method to use the HttpClient() object instead of the WebClient object. The HttpClient has only asynchronous APIs, so we call the GetStringAsync API.
The keen eyed would note we are not awaiting the calls, so we are getting a Task from the GetStringAsync method.
Next we have a await for Task.WhenAll(…) and pass all the Task instances to it. What this does is, it retrieves all the Tasks and kicks them off together and waits for all of them to return. Once they all return, we create the Dashboard object and return it to the Action Method.
public async Task GetFeeds()
{
var emails = new HttpClient().GetStringAsync("http://localhost:18545/api/Emails");
var myTasks = new HttpClient().GetStringAsync("http://localhost:18545/api/Tasks");
var notes = new HttpClient().GetStringAsync("http://localhost:18545/api/Notes");
var bookmarks = new HttpClient().GetStringAsync("http://localhost:18545/api/Bookmarks");
await Task.WhenAll(emails, myTasks, notes, bookmarks);
Dashboard dash = new Dashboard();
dash.Emails = Deserialize(emails.Result);
dash.Bookmarks = Deserialize(bookmarks.Result);
dash.Notes = Deserialize(notes.Result);
dash.Tasks = Deserialize(myTasks.Result);
return dash;
}
We update the Index action method to be called async as well and it now returns a Task instead of ActionResult.
public async Task Index()
{
//Services.FeedService feedService = new Services.FeedService();
Stopwatch timer = Stopwatch.StartNew();
timer.Start();
var feeds = await GetFeeds();
timer.Stop();
feeds.TimeTaken = timer.ElapsedMilliseconds;
return View(feeds);
}
With that, we’ve converted our application to Async all throughout. Moment of truth, Time to test the outcome.
Load Testing Asynchronous implementation
Do a Clean Build and hit Ctrl + F5 to run the application without debugger. Open the LoadTest1.loadtest file and Run the Load Test.
As we can see, the Average Page Time is 1.17 and the Page Count is 254 - almost threefold increase if the number of pages hit and the 1/3 the Average Page time. This validates our belief, but there is more than meets the eye here.
Understanding Performance Changes
The improved scaling of the app resulting in the higher Count of requests handled is understandable, but how did the page time drop? Well, remember the bunching up of Tasks and then firing them off with the Tasks.WhenAll? Yup, that’s where I cheated by launching Async tasks in parallel (more threads so BEWARE). If I awaited each request to the web service, I would still get improved performance, but the Average Page Time would be similar to Synchronous operation as the following image shows (Page time of 2.92 and Request Count of 108).
So yes, Async and Parallel Async are different and affects scaling and performance differently.
The Final Confession (I cheated, but only to prove a point)
Performance testing is tough, performance testing on a restricted VM is even tougher. To be honest, I couldn’t find enough performance difference for the Four Database calls (there is a caveat regarding database calls and async but that’s coming in a minute), hence to get some visible difference, I put a Thread.Sleep(500) for the synchronous calls and Task.Wait(500) for the asynchronous calls. Hence you see the 2 second+ Page times. Apart from making the performance difference obvious, I believe it doesn’t affect the conclusions.
- Synchronous calls to 4 services takes 2+ seconds and only 70 odd Page Requests were serviced
- Asynchronous calls with await for each service calls still takes 2+ seconds but handles 100+ Page requests.
- Asynchronous calls fired off in parallel take < 2 seconds (1.17 seconds) because there are 2 cores in action so effectively the 4 requests were handled by two parallel threads reducing the page load time and handling more Page Requests.
The Final Set of Caveats
Okay, so async and multi-threading usually gets all hairy and controversial if the caveats are not called out. I already called out one where the Task.WhenAll was potentially spinning up more than one physical thread to service one request. This can get dangerous and lead to Threadpool starvation in case of very high load.
Next, by making your Service calls async, you are moving the performance bottleneck from the Web Tier to the DB Tier (in our specific example). This could come back to bite you with increased DB server load. So use async on DB server cautiously.
However, if your services are going out to the Internet to fetch data (like from RSS feeds) you are good, because in that case, you will be firing more requests to an external service (which may be subject to its own rate limits) and won’t be killing any of your internal systems. In a nutshell, if the process is really long running and not stressing out some other part of your architecture, async is a good candidate to improving scaling of your web tier.
Conclusion
With that I conclude this article. Hopefully you’ve had a few takeaways from the demonstration, primarily:
- How to use aync APIs end to end starting with MVC Controller right down to Database calls via Async Web API services
- How to create a LoadTest project and fire load tests at your application
- Difference between scaling and performance gains when using Async.
Download the entire source code of this article here (Github)
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of
DotNetCurry,
DNC Magazine for Developers,
SQLServerCurry and
DevCurry. He has also authored a couple of books
51 Recipes using jQuery with ASP.NET Controls and
The Absolutely Awesome jQuery CookBook.
Suprotim has received the prestigious Microsoft MVP award for Sixteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.
Get in touch with him on Twitter @suprotimagarwal or at LinkedIn