Asynchronous programming in action

c# net-7

Jan 13 2024 04:17

If you have any I/O-bound needs (such as requesting data from a network, accessing a database, or reading and writing to a file system), you'll want to utilize asynchronous programming. You could also have CPU-bound code, such as performing an expensive calculation, which is also a good scenario for writing async code.

Key pieces to understand
  • Async code can be used for both I/O-bound and CPU-bound code, but differently for each scenario.
  • Async code uses Task and Task, which are constructs used to model work being done in the background.
  • The async keyword turns a method into an async method, which allows you to use the await keyword in its body.
  • When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete.
  • await can only be used inside an async method.
Here are two questions you should ask before you write any code
  1. Will your code be "waiting" for something, such as data from a database? If your answer is "yes", then your work is I/O-bound.
  2. Will your code be performing an expensive computation? If you answered "yes", then your work is CPU-bound.
Blocking vs Non-Blocking API

For example, I use HttpClient which is a class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.

HttpClient provides only non-blocking API, let's see this function Because GetAsync is non-blocking, so stopwatch.Stop() execute right away and results always < 20 milliseconds on my machine

public void GetProduct()
{
    var client = new HttpClient();
    Stopwatch stopwatch = new Stopwatch();

    stopwatch.Start();
    client.GetAsync("https://dummyjson.com/products/1");
    client.GetAsync("https://dummyjson.com/products/2");
    client.GetAsync("https://dummyjson.com/products/3");
    stopwatch.Stop();

    Console.WriteLine(stopwatch.ElapsedMilliseconds); // Always < 20ms
}

If you want to calculate the total time of calling API in sequence, just simply add await before non-blocking API

await client.GetAsync("https://dummyjson.com/products/1");
await client.GetAsync("https://dummyjson.com/products/2");
await client.GetAsync("https://dummyjson.com/products/3");

But await is only available in async function, you need to add async keyword too, And an async method with a void return type does not follow the task asynchronous programming (TAP) model, we need replace void with Task

public async Task GetProduct()

The whole function became, execute time is huge (~1100 milliseconds) different, because Stopwatch needs to wait for all API calls to finish

public async Task GetProduct()
{
    var client = new HttpClient();
    Stopwatch stopwatch = new Stopwatch();

    stopwatch.Start();
    await client.GetAsync("https://dummyjson.com/products/1");
    await client.GetAsync("https://dummyjson.com/products/2");
    await client.GetAsync("https://dummyjson.com/products/3");
    stopwatch.Stop();

    Console.WriteLine(stopwatch.ElapsedMilliseconds); // About ~1100 ms
}
    }

To use the benefit of Asynchronous Programming/ non-blocking API, we can remove the await block on each API and use one await for all API calls by Task.WhenAll()

public async Task GetProduct()
{
    var client = new HttpClient();
    Stopwatch stopwatch = new Stopwatch();

    stopwatch.Start();
    Task<HttpResponseMessage> getProductTask1 = client.GetAsync("https://dummyjson.com/products/1");
    Task<HttpResponseMessage> getProductTask2 = client.GetAsync("https://dummyjson.com/products/2");
    Task<HttpResponseMessage> getProductTask3 = client.GetAsync("https://dummyjson.com/products/3");

    await Task.WhenAll(getProductTask1, getProductTask2, getProductTask3);
    stopwatch.Stop();

    Console.WriteLine(stopwatch.ElapsedMilliseconds); // About ~780 ms
}

Execute time is about ~780ms, all data is fetched, and significantly time reduced compared to blocking call

Important info and advice
  • async methods need to have an await keyword in their body or they will never yield!
  • Add "Async" as the suffix of every async method name you write.
  • Write code that awaits Tasks in a non-blocking manner
    Use this... Instead of this... When wishing to do this...
    await Task.Wait or Task.Result Retrieving the result of a background task
    await Task.WhenAny Task.WaitAny Waiting for any task to complete
    await Task.WhenAll Task.WaitAll Waiting for all tasks to complete
    await Task.Delay Thread.Sleep Waiting for a period of time

More important info and advice at Microsoft Learn

me

Pham Duc Minh

Da Nang, Vietnam