Async Calls Using the async/await Pattern

A First Look at the C# async and await Keywords

The async keywords of C# is used to qualify that a method, lambda expression, or anonymous method should be called in an anonymous manner automatically.


Simply by marking a method with the async modifier, the .NET Core Runtime will create a new thread of execution to handle the task at hand. Furthermore, when you are calling an "async" method, the "await" keyword will automatically pause the current thread from any further activity until the task is complete, leaving the current thread free to continue.


string m = DoWork();
static string DoWork()
 {
     Thread.Sleep(5_000);
     return "done";
 }


If this were a gui, the entire screen would be locked.

string m = await DoWork();
static async Task<string> DoWork()
 {
     return await Task.Run(() =>
     {
         Thread.Sleep(5000);
         return "done";
     });
 }

If you are using a Main() method as your entry point, you need to mark the method async.


static async Task Main(string[] args)
 {
     string m = await DoWork();
     Console.WriteLine(m);
 }


Note: Top-level statements, added in C# 9.0, are implicitly async.


SynchronizationContext and async/await

The official definition of SynchronizationContext is a base class that provides free-threaded context with no synchronization.


The purpose of synchronization model implemented by this class is to allow the internal asynchronous/synchronous operations of the common language runtime to behave properly with different synchronization models.


This class is a type that provides a virtual post method, which takes a delegate to be executed asynchronously. This provides a pattern for frameworks to appropriately handle asynchronous requests (dispatching for WPF/WinForms, directly executing for non-GUI apps). It provides a way to queue a unit of work to a context and keeps cout of outstanding async operations.


The Role of ConfigureAwait

string m = await DoWork();

string m1 = await DoWork().ConfigureAwait(false);


The original code block is using the SynchronizationContext supplied by the framework. It's equivalent to calling ConfigureAwait(true). The 2 example ignores the current context and scheduler.


Async Methods That Don't Return Data

There are 2 ways:

  1. Async void methods:


static async void MethodReturningVoidAsync()
 {
     await Task.Run(() =>
     {
         Thread.Sleep(4_000);
     });
 }
MethodReturningVoidAsync();


  1. Async void Methods Using Task


static async Task MethodReturningTaskAsync()
 {
     await Task.Run(() =>
     {
         Thread.Sleep(4_000);
     });
 }
 
await MethodReturningTaskAsync();



When you run the program and exception is thrown, 2 interesting things happen. The first is that the whole program doesn't crash, and the second is that the catch block doesn't catch th exception. When an exception is thrown out of a Task/Task<T> method, the exception is captured and placed on the Task object. When using await, the Exception is available to handle.


Async Methods With Multiple Awaits

This option is not await each task but await them all together and return when all of the tasks are completed.


await Task.WhenAll(Task.Run(() =>
     {
         Thread.Sleep(2000);
     }), Task.Run(() =>
     {
         Thread.Sleep(3_000);
     })
 );


There is also a WhenAny(), which signals that one of the tasks have completed. The method returns the first task that completed.


await Task.WhenAny(Task.Run(() =>
     {
         Thread.Sleep(2000);
     }), Task.Run(() =>
     {
         Thread.Sleep(3_000);
     })
 );


Calling async Method from Synchronous Methods

Await only works inside async methods.


static async Task<string> DoWorkAsync()
 {
     await Task.Delay(1000);
     return "Done";
 }
 
// WAY 1 - FIRE AND FORGET (BAD)
void SyncMethod()
 {
     DoWorkAsync(); // no await no result
     // return value lost, exceptions swallowed silently.
 }
 
// WAY 2 - .Result() / .Wait()    BAD-DEADLOCK RISK
void SyncMethod()
 {
     string result = DoWorkAsync().Result; // Task<T> -> .Result
     SomeTaskMethod().Wait(); // Task -> .Wait()
     // Blocks calling thread - wastes 2 threads for 1 job.
     // Exceptions wrapped in AggerateException
     // UI Thread - DEADLOCk
 }
 
// WAY 3 - GetAwaiter().GetResult() (BAD-DEADLOCK RISK)
void SyncMethod()
 {
     string result = DoWorkAsync().GetAwaiter().GetResult();
     // Same as .Result BUT:
     // Does NOT wrap exception in AggregateException (throws directly)
     // Marked "not for external use" in docs.
 }
 
 
// WAY 4 - JoinableTaskFactory.Run()   (CORRECT!)
void SyncMethod()
 {
     var jtf = new JoinableTaskFactory(new JoinableTaskContext());
     string result = jtf.Run(async () => await DoWorkAsync()); // Task<T> has return value.
     // Task (void return).
     jtf.Run(async () =>
     {
         await MethodReturningTaskAsync();
         await SomeOtherAsyncMethod();
     });
 }


Await in catch and finally Blocks

The method itself must be async to do this.

static async Task<string> MethodWithTryCatch()
 {
     try
     {
         //
         return "Hello";
     }
     catch (Exception)
     {
         await LogTheErrors();
         throw;
     }finally
     {
         await DoMagicCleanUp();
     }
 }


Generalized Async Return Types

C# 7 enables additional return types, if they follow the async pattern. One concrete example is ValueTask.


static async ValueTask<int> ReturnAnInt()
 {
     await Task.Delay(1000);
     return 5;
 }


The same rules apply for ValueTask as for Task, since ValueTask is just a Task for value types instead of forcing allocation of an object on the heap.


Local Async Functions

Method inside a method, can be async.


async Task SaveDataAsync(string data)
 {
     async Task DoSave()
     {
         await Task.Delay(1000);
         Console.WriteLine(data);
     }
     await DoSave();
 }


Cancelling async/await

CancellationTokenSource = Remote Control

CancellationToken = Receiver inside method

ThrowIfCancelletionRequested() = "am I cancelled?"


CancellationTokenSource cts = new();
cts.CancelAfter(3000);
try
 {
     await DoWorkAsync(cts.Token);
 }
catch (OperationCanceledException)
 {
     throw;
 }
 
 
 private async Task DoWorkAsync(CancellationToken token)
  {
      for(int i = 0; i < 100; i++)
      {
          token.ThrowIfCancellationRequested();
          await Task.Delay(500, token); // Pass token here too!
          Console.WriteLine(i);
      }
  }


WaitAsync() Method

Cancel the WAIT, not the work itself.

Use when method doesn't accept CancellationToken.


Task<string> longTask = ThirtPartyMethodAsync();
// cancel by token
CancellationTokenSource cts = new();
try
 {
     string result = await longTask.WaitAsync(cts.Token);
 }
catch (Exception)
 {
     Console.WriteLine("Gave up"); // longtask still running in background.
 }
 
// cancel by timeout
CancellationTokenSource cts = new();
try
 {
     string result = await longTask.WaitAsync(TimeSpan.FromSeconds(3));
 }
catch (Exception)
 {
     Console.WriteLine("Timeout");
 }

Cancel Async from Sync Context

JoinableTaskFactory + CancellationToken = safe combo

void SyncMethod()
 {
     CancellationTokenSource cts = new();
     cts.CancelAfter(5000);
     JoinableTaskFactory jtf = new(new JoinableTaskContext());
     try
     {
         jtf.Run(async () => await DoWorkAsync(cts.Token));
     }
     catch (Exception)
     {
         Console.WriteLine("Cancelled from sync");
     }finally
     {
         cts.Dispose(); 
     }
 }



Parallel.ForEachAsync()

Parallel.ForEach but with real await inside.

ForEach = CPU-band

ForEachAsync = 20-bound.


string[] urls = { "https://a.com", "https://b.com" };
HttpClient http = new HttpClient();
await Parallel.ForEachAsync(urls, new ParallelOptions
 {
     MaxDegreeOfParallelism = 3
 }, async (url, token) =>
 {
     string html = await http.GetStringAsync(url, token);
     Console.WriteLine($"{url} {html.Length} chars");
 });


With the CancellationTokenSource:


CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
string[] urls = { "https://a.com", "https://b.com" };
HttpClient http = new HttpClient();
await Parallel.ForEachAsync(urls, new ParallelOptions
 {
     MaxDegreeOfParallelism = 3,
     CancellationToken = cts.Token
 }, async (url, token) =>
 {
     string html = await http.GetStringAsync(url, token);
     Console.WriteLine($"{url} {html.Length} chars");
 });