I wrote a couple of action methods in a controller to test the difference between sync and async controller actions in ASP.NET core:
[Route("api/syncvasync")]
public class SyncVAsyncController : Controller
{
[HttpGet("sync")]
public IActionResult SyncGet()
{
Task.Delay(200).Wait();
return Ok(new { });
}
[HttpGet("async")]
public async Task<IActionResult> AsyncGet()
{
await Task.Delay(200);
return Ok(new { });
}
}
I then load tested the sync end point:
... followed by the async end point:
Here's the results if I increase the delay to 1000 ms
As you can see there is not much difference in the requests per second - I expected the async end point to handle more requests per second. Am I missing something?
Yes, you are missing the fact that async is not about speed, and is only slightly related to the concept of requests per second.
Async does one thing and only one thing. If a task is being awaited, and that task does not involve CPU-bound work, and as a result, the thread becomes idle, then, that thread potentially could be released to return to the pool to do other work.
That's it. Async in a nutshell. The point of async is to utilize resources more efficiently. In situations where you might have had threads tied up, just sitting there tapping their toes, waiting for some I/O operation to complete, they can instead be tasked with other work. This results in two very important ideas you should internalize:
Async != faster. In fact, async is slower. There's overhead involved in an asynchronous operation: context switching, data being shuffled on and off the heap, etc. That adds up to additional processing time. Even if we're only talking microseconds in some cases, async will always be slower than an equivalent sync process. Period. Full stop.
Async only buys you anything if your server is at load. It's only at times when your server is stressed that async will give it some much needed breathing room, whereas sync might bring it to its knees. It's all about scale. If your server is only fielding a minuscule amount of requests, you very likely will never see a difference over sync, and like I said, you may end up using more resources, ironically, because of the overhead involved.
That doesn't mean you shouldn't use async. Even if your app isn't popular today, it doesn't mean it won't be later, and rejiggering all your code at that point to support async will be a nightmare. The performance cost of async is usually negligible, and if you do end up needing it, it'll be a life-saver.
UPDATE
In the regard of keeping the performance cost of async negligible, there's a few helpful tips, that aren't obvious or really spelled out that well in most discussions of async in C#.
Use ConfigureAwait(false)
as much as you possibly can.
await DoSomethingAsync().ConfigureAwait(false);
Pretty much every asynchronous method call should be followed by this except for a few specific exceptions. ConfigureAwait(false)
tells the runtime that you don't need the synchronization context preserved during the async operation. By default when you await an async operation an object is created to preserve thread locals between thread switches. This takes up a large part of the processing time involved in handling an async operation, and in many cases is completely unnecessary. The only places it really matters is in things like action methods, UI threads, etc - places where there's information tied to the thread that needs to be preserved. You only need to preserve this context once, so as long as your action method, for example, awaits an async operation with the synchronization context intact, that operation itself can perform other async operations where the synchronization context is not preserved. Because of this, you should confine uses of await
to a minimum in things like action methods, and instead try to group multiple async operations into a single async method that that action method can call. This will reduce the overhead involved in using async. It's worth noting that this is only a concern for actions in ASP.NET MVC. ASP.NET Core utilizes a dependency injection model instead of statics, so there are no thread locals to be concerned about. In others you can use ConfigureAwait(false)
in an ASP.NET Core action, but not in ASP.NET MVC. In fact, if you try, you'll get a runtime error.
As much as possible, you should reduce the amount of locals that need to be preserved. Variables that you initialize before calling await are added to the heap and the popped back off once the task has completed. The more you've declared, the more that goes onto the heap. In particular large object graphs can wreck havoc here, because that's a ton of information to move on and off the heap. Sometimes this is unavoidable, but it's something to be mindful of.
When possible, elide the async
/await
keywords. Consider the following for example:
public async Task DoSomethingAsync()
{
await DoSomethingElseAsync();
}
Here, DoSomethingElseAsync
returns a Task
that is awaited and unwrapped. Then, a new Task
is created to return from DoSometingAsync
. However, if instead, you wrote the method as:
public Task DoSomethingAsync()
{
return DoSomethingElseAsync();
}
The Task
returned by DoSomethingElseAsync
is returned directly by DoSomethingAsync
. This reduces a significant amount of overhead.