C# — Async and Await — How does it work?

Robin Ding
5 min readSep 7, 2021
  1. How does it work?
  2. Best Practices
  3. IO Based vs CPU Based
  4. SynchronizationContext & TaskScheduler
  5. Deadlock
  6. ConfigureAwait(false) & FodyWeavers
  7. How to call async method syncly & How to call sync method asyncly?
  8. 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.

  1. A calling method calls and awaits the GetUrlContentLengthAsync async method.
  2. GetUrlContentLengthAsync creates an HttpClient instance and calls the GetStringAsync asynchronous method to download the contents of a website as a string.
  3. 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.
  4. GetStringAsync returns a Task<TResult>, where TResult is a string, and GetUrlContentLengthAsync assigns the task to the getStringTask variable. The task represents the ongoing process for the call to GetStringAsync, with a commitment to produce an actual string value when the work is complete.
  5. Because getStringTask hasn't been awaited yet, GetUrlContentLengthAsync can continue with other work that doesn't depend on the final result from GetStringAsync. That work is represented by a call to the synchronous method DoIndependentWork.
  6. DoIndependentWork is a synchronous method that does its work and returns to its caller.
  7. GetUrlContentLengthAsync has run out of work that it can do without a result from getStringTask. 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.
  8. Therefore, GetUrlContentLengthAsync uses an await operator to suspend its progress and to yield control to the method that called GetUrlContentLengthAsync. GetUrlContentLengthAsync returns a Task<int> to the caller. The task represents a promise to produce an integer result that's the length of the downloaded string.
  9. Note
  10. If GetStringAsync (and therefore getStringTask) completes before GetUrlContentLengthAsync awaits it, control remains in GetUrlContentLengthAsync. The expense of suspending and then returning to GetUrlContentLengthAsync would be wasted if the called asynchronous process getStringTask has already completed and GetUrlContentLengthAsync doesn't have to wait for the final result.
  11. 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 for GetUrlContentLengthAsync, and GetUrlContentLengthAsync is waiting for GetStringAsync.
  12. GetStringAsync completes and produces a string result. The string result isn't returned by the call to GetStringAsync 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 from getStringTask. The assignment statement assigns the retrieved result to contents.
  13. When GetUrlContentLengthAsync has the string result, the method can calculate the length of the string. Then the work of GetUrlContentLengthAsync 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?

  1. 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.
  2. 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.
  3. 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.
  4. The behind logic of async, await likes a queue + pub/sub pattern
  5. 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.
  6. When we call await, .NET Framework subscribe the complete message from the async method.

I/O Bound Operation vs. CPU-Bound Operation

  1. For I/O Bound Operation, we would better to use async and await
  2. 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:

  1. When we call async, it will execute based on the current thread
  2. 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.

References

  1. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model
  2. https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth

--

--