C# — Async and Await — How does it work?
5 min readSep 7, 2021
- How does it work?
- Best Practices
- IO Based vs CPU Based
- SynchronizationContext & TaskScheduler
- Deadlock
- ConfigureAwait(false) & FodyWeavers
- How to call async method syncly & How to call sync method asyncly?
- Comparison (Console, WinForm, ASP.NET MVC, ASP.NET WebAPI, ASP.NET Core)
Introduction
async & await was introduced in C# 5, which is supported for .NET Framework 4.5+, .NET Core and Windows Runtime. Let’s get started with an example:
public async Task<int> GetUrlContentLengthAsync(){
var client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("https://docs.microsoft.com/dotnet");
DoIndependentWork();
string contents = await getStringTask;
return contents.Length;
}
void DoIndependentWork(){
Console.WriteLine("Working ...");
}
How does it work?
(Below are from Microsoft Blog)
The numbers in the diagram correspond to the following steps, initiated when a calling method calls the async method.
- A calling method calls and awaits the
GetUrlContentLengthAsync
async method. GetUrlContentLengthAsync
creates an HttpClient instance and calls the GetStringAsync asynchronous method to download the contents of a website as a string.- Something happens in
GetStringAsync
that suspends its progress. Perhaps it must wait for a website to download or some other blocking activity. To avoid blocking resources,GetStringAsync
yields control to its caller,GetUrlContentLengthAsync
. GetStringAsync
returns a Task<TResult>, whereTResult
is a string, andGetUrlContentLengthAsync
assigns the task to thegetStringTask
variable. The task represents the ongoing process for the call toGetStringAsync
, with a commitment to produce an actual string value when the work is complete.- Because
getStringTask
hasn't been awaited yet,GetUrlContentLengthAsync
can continue with other work that doesn't depend on the final result fromGetStringAsync
. That work is represented by a call to the synchronous methodDoIndependentWork
. DoIndependentWork
is a synchronous method that does its work and returns to its caller.GetUrlContentLengthAsync
has run out of work that it can do without a result fromgetStringTask
.GetUrlContentLengthAsync
next wants to calculate and return the length of the downloaded string, but the method can't calculate that value until the method has the string.- Therefore,
GetUrlContentLengthAsync
uses an await operator to suspend its progress and to yield control to the method that calledGetUrlContentLengthAsync
.GetUrlContentLengthAsync
returns aTask<int>
to the caller. The task represents a promise to produce an integer result that's the length of the downloaded string. - Note
- If
GetStringAsync
(and thereforegetStringTask
) completes beforeGetUrlContentLengthAsync
awaits it, control remains inGetUrlContentLengthAsync
. The expense of suspending and then returning toGetUrlContentLengthAsync
would be wasted if the called asynchronous processgetStringTask
has already completed andGetUrlContentLengthAsync
doesn't have to wait for the final result. - Inside the calling method the processing pattern continues. The caller might do other work that doesn’t depend on the result from
GetUrlContentLengthAsync
before awaiting that result, or the caller might await immediately. The calling method is waiting forGetUrlContentLengthAsync
, andGetUrlContentLengthAsync
is waiting forGetStringAsync
. GetStringAsync
completes and produces a string result. The string result isn't returned by the call toGetStringAsync
in the way that you might expect. (Remember that the method already returned a task in step 3.) Instead, the string result is stored in the task that represents the completion of the method,getStringTask
. The await operator retrieves the result fromgetStringTask
. The assignment statement assigns the retrieved result tocontents
.- When
GetUrlContentLengthAsync
has the string result, the method can calculate the length of the string. Then the work ofGetUrlContentLengthAsync
is also complete, and the waiting event handler can resume. In the full example at the end of the topic, you can confirm that the event handler retrieves and prints the value of the length result. If you are new to asynchronous programming, take a minute to consider the difference between synchronous and asynchronous behavior. A synchronous method returns when its work is complete (step 5), but an async method returns a task value when its work is suspended (steps 3 and 6). When the async method eventually completes its work, the task is marked as completed and the result, if any, is stored in the task.
Why async & await?
- Using async & await will improve responsiveness, and won’t block current thread, which is really helpful for I/O-Bound operations. Thread doesn’t have to wait the I/O Operation complete, instead, the thread can work on something else, i.e. DoIndependentWork() as above code. Keep in mind, we need to put DoIndependentWork() before await.
- The async and await keywords don’t cause additional threads to be created. Async methods don’t require multithreading because an async method doesn’t run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn’t help with a process that’s just waiting for results to become available.
- The async-based approach to asynchronous programming is preferable to existing approaches in almost every case. In particular, this approach is better than the BackgroundWorker class for I/O-bound operations because the code is simpler and you don’t have to guard against race conditions. In combination with the Task.Run method, async programming is better than BackgroundWorker for CPU-bound operations because async programming separates the coordination details of running your code from the work that
Task.Run
transfers to the thread pool. - The behind logic of async, await likes a queue + pub/sub pattern
- When we call async, .NET Framework puts the async method & synchronization context into the queue, it will be executed based on the resource (Thread Pool) we have setup.
- When we call await, .NET Framework subscribe the complete message from the async method.
I/O Bound Operation vs. CPU-Bound Operation
- For I/O Bound Operation, we would better to use async and await
- For CPU-Bound Operation, we would better to use Task.Run(…)
ASP.NET MVC Example
public class AsyncTester
{
public Action<string> WriteLine { get; set; }
public async Task TestAsync()
{
var granicusDownloadTask = DownloadAsync("https://www.bing.com");
WriteLine($"In between");
var googleDownloadTask = DownloadAsync("https://www.google.com");
await Task.WhenAll(granicusDownloadTask, googleDownloadTask);
}
async Task<string> DownloadAsync(string url)
{
var html = string.Empty;
using (var webClient = new WebClient())
{
html = await webClient.DownloadStringTaskAsync(url);
}
WriteLine($"async Task<int> DownloadAsync({url})");
WriteLine($"{url}: {html.Substring(0, 5)} ");
return html;
}
}
// HomeController
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Title = "Home Page";
return View();
}
[HttpGet]
public async Task<ActionResult> ShowResultAsync()
{
var result = new List<string>();
Action<string> WriteLine = str => result.Add($"Thread Id: {Thread.CurrentThread.ManagedThreadId} IsBackground:{Thread.CurrentThread.IsBackground} IsThreadPoolThread:{Thread.CurrentThread.IsThreadPoolThread} - {str}");
WriteLine("111");
var asyncTester = new AsyncTester() { WriteLine = WriteLine };
var task = asyncTester.TestAsync();
await task;
WriteLine("222");
return Json(result.ToArray(), JsonRequestBehavior.AllowGet);
}
}
Test Result
0 "Thread Id: 9 IsBackground:True IsThreadPoolThread:True - 111"
1 "Thread Id: 9 IsBackground:True IsThreadPoolThread:True - In between"
2 "Thread Id: 15 IsBackground:True IsThreadPoolThread:True - async Task<int> DownloadAsync(https://www.google.com)"
3 "Thread Id: 15 IsBackground:True IsThreadPoolThread:True - https://www.google.com: <!doc "
4 "Thread Id: 13 IsBackground:True IsThreadPoolThread:True - async Task<int> DownloadAsync(https://www.bing.com)"
5 "Thread Id: 13 IsBackground:True IsThreadPoolThread:True - https://www.bing.com: \n\n<!D "
6 "Thread Id: 13 IsBackground:True IsThreadPoolThread:True - 222"
From the result:
- When we call async, it will execute based on the current thread
- When we call await, it will wait the async method complete then get the control, and execute within a new thread. (in ASP.NET MVC)
More details will be explained in the future post regarding SynchronizationContext.Current and TaskScheduler.Current.