Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Mar 28

Asynchronous programming with async and await in C# — Part 1

Автор:
Источник:
Просмотров:
480

Introduction

Welcome to the world of asynchronous programming with C#. In this enlightening series, we’ll explore the intricacies of async and await keywords, unraveling the magic behind writing clean and efficient code for asynchronous operations in C#. This first part delves into the theory behind async programming, dissecting the essential components and shedding light on the control flow of async methods.

Async in depth

First of all, the async-await keyword pair was introduced in C# 5 as a way of writing cleaner, synchronous-like reading code for asynchronous operations. By using the async & await keywords, a method can be written very similar in structure to standard synchronous code, but can work asynchronously without keeping the calling thread busy.

An async method looks like a synchronous one in structure with a few differences:

  • The method signature is marked with async.
  • The method uses await on a Task to get back some type of result of an asynchronous operation.
  • The return type is Task<T> or Task rather than T or void.
  • The method name has Async added as a suffix.

Let’s now examine what happens, on a high level, when the execution of our code hits the line with await using an example. Consider the following code:

public async Task<int> GetMessageLengthAsync()
{
    Task<string> stringTask = GetTimeTakingMessageAsync(); // calls a long running operation
    DoIndependentWork(); // does some independent work meanwhile
    string message = await stringTask; // suspends & awaits the result
    int length = message.Length; // once done, does some work on the awaited result
    return length; // then returns the final int result to the caller
}

private async Task<string> GetTimeTakingMessageAsync()
{
    await Task.Delay(3000);
    return "Some message";
}

private void DoIndependentWork()
{
    Console.WriteLine("Doing some synchronous work.");
}

The method GetMessageLengthAsync() calls the long-running GetTimeTakingMessageAsync() method and gets back a Task<string>, which resembles work to be done in the background. While that Task is being processed, GetMessageLengthAsync continues execution and now calls the DoIndependentWork() method, which is independent of the background Task value.

When execution arrives at the await messageTask line, first the execution checks if the work has already completed and if it does, it simply continues executing it as normal synchronous method. If the work has not yet been completed, the GetMessageLengthAsync() method suspends the current work-flow, captures the current context and waits for the long-running task to complete asynchronously, without blocking the calling thread.

Now, the thread that originally had called GetMessageLengthAsync(), gets free at this point, leaves the method and gives control back to the original calling location. Once the Task is completed, the method resumes from the await point on the captured context, does some processing dependent on the Task result (int length = message.Length) and finally returns the int result.

Let’s now dive into the control flow of an async method and in particular the one we presented in the previous example.

Async flow example

We have the following steps in the control flow:

  1. First an external method, say CallingMethod() calls our GetMessageLengthAsync() method. Let’s call the thread that makes this call as the main thread (the green lines).
  2. GetMessageLengthAsync() makes a call to another async method on the same thread, just like any normal code execution.
  3. The GetTimeTakingMessageAsync() method being an async method, returns control immediately to its caller (GetMessageLengthAsync), leaving the work of getting message to be done in background.
  4. GetMessageLengthAsync() continues its execution normally and makes a call to a method called DoIndependentWork() that does not depend on the message result from GetTimeTakingMessageAsync(). This is called synchronously on the main thread.
  5. After DoIndependentWork() completes, control of execution comes back to GetMessageLengthAsync().
  6. The execution hits the line with the await keyword, which instructs the method to wait asynchronously (not block) for the message. At await, the main thread suspends the work on this method, and returns to CallingMethod() where it is free to do other stuff. Just before this, CLR captures what is called the synchronization context (more on that on another part of the series).
  7. At a later point of time, when the message is fully received, the continuation of the rest of the code is invoked after the await keyword on GetMessageLengthAsync(). Depending on the captured context, the rest of the code might run on the same main thread, or another thread.
  8. Finally, the rest of the code is executed and upon completion the final result is returned to CallingMethod().

Key pieces to understand

There are some key pieces we need to understand from all of these:

  • The core of async programming is the Task and Task<T> objects, which model asynchronous operations. A method can be await-ed from another method, because the await-ed method can return a Task or a Task<T> object and not because it has async inside its signature, or it uses await inside its body.
  • The async enables the await functionality in the method. This simply means that you cannot type await without using the async declaration on the method signature. On the other hand, a method can be declared as async without using await in the method body, but then the method just runs synchronously.
  • The await keyword is the part which turns the method asynchronous and allows a UI to be responsive or a service to be elastic. Your code does not need to rely on callbacks or events to continue execution after the task completion . If you’re using Task<T>, the await keyword will additionally “unwrap” the value returned when the Task is complete.

Threads in Async-Await & recognizing CPU-bound and I/O-bound work

You may be thinking now, what about threads in async-await. Actually, a very common question around async-await is whether there any new threads created for an asynchronous process to execute. The answer is no, but there might or might not be additional threads involved. If additional threads are required, generally threads are taken from the managed ThreadPool.

Now comes another question: When is a thread pool thread used? If a thread pool thread is not used, how does the asynchronous work complete without a thread? To answer this question, first of all we need to distinguish between two types of work that are done asynchronously — CPU bound and I/O bound.

CPU bound work is something that does some heavy computation. It needs continuous CPU involvement and for that it uses a dedicated thread and will generally use a ThreadPool thread. I/O bound work, on the other hand, is something that depends on something outside of the CPU. It may not need any dedicated threads (e.g. network or disk driver may handle the work by themselves). Only few time-slices of thread(s) are used just for start/stop/progress notifications.

Recognize CPU-bound and I/O-bound work

Now a question for you. Which of the following is CPU-bound and which one is I/O-bound work?

  • Your code needs time to get response from a web service and then write the data from the response to the disk.
  • Your code needs to run some formula along a huge set of data.

Pause for a bit and think about it. Ready?

So, eventually the question really is how do we recognize CPU-bound and I/O-bound work. Here are two questions you should ask before you write any code:

  • Will your code be “waiting” for some sort of out-of-process operation, such as data from a database, network or file-system? If the answer is “Yes”, then it is I/O bound work.
  • Will your code be performing some sort of in-process operation, such as an expensive computation? If the answer is “Yes”, then it is CPU bound work.

Task and Task<T> for an I/O-Bound operation

Let’s deep dive on this even further, by looking at two different examples of using Tasks and async/await pattern for an I/O bound operation and for a CPU bound operation. First things first, consider the following I/O bound operation code snippet and think for yourself about the following question: Does the call to GetStringAsync() need to execute on a different dedicated thread?

public async Task<string> GetFirstCharactersAsync(string url, int count)
{
    // Execution is synchronous here
    using var client = new HttpClient();

    // Execution of GetFirstCharactersAsync() is yielded to the caller here
    // GetStringAsync returns a Task<string>, which is *awaited*
    var page = await client.GetStringAsync(url);

    // Execution resumes when the client.GetStringAsync task completes,
    // becoming synchronous again.
    return count > page.Length ?
        page : page[..count];
}

Throughout the process of executing a specific task asynchronously, a key takeaway is that no thread is dedicated to running the task. Although work is executed in some context (the OS does have to pass data to a device driver and respond to an interrupt), there is no thread dedicated to waiting for data from the request to come back.

A potential timeline for such a call would look like this:

  • 1st part: Time spent for everything up until an async method arrives at an await & yields control back to its caller.
  • 2nd part: Time spent on I/O, with no CPU cost.
  • 3rd part: Time spent for passing control back (and potentially a value) to the async method, at which point it continues its execution synchronously (for the remaining part after the await).

Have a look at the following I/O operation code snippet example as well:

public async Task<string> DownloadString(string url)
{
    var client = new HttpClient();
    var request = await client.GetAsync(url);
    var download = await request.Content.ReadAsStringAsync();
    return download;
}

The sequence of how this gets executed is the following:

  • Calling client.GetAsync(url) will create a request behind the scenes by calling lower-level .NET libraries.
  • Some part of its underlying code may run synchronously until it delegates its work from the networking APIs to the OS.
  • At this point, a Task gets created and gets bubbled up to the original caller of the asynchronous code. This is still an unfinished Task.
  • Once the network request is completed by the OS level, the response gets returned via an I/O completion port and CLR gets notified by a CPU interrupt that the work is done.
  • The response gets scheduled to be handled by the next available thread to unwrap the data.
  • The remainder of your async method continues running synchronously.

This simply means once again, that there won’t be any dedicated thread to complete the I/O Task. Which also means that your task continuation isn’t guaranteed to run on the same thread that started it.

For a server scenario, this means that, because there are no threads dedicated to blocking on unfinished tasks, the server thread pool can service a much higher volume of web requests. Thus, a server is expected to be able to handle more requests using async and await than if it were dedicating a thread for each request it received.

For a client scenario, this means increased responsiveness by not blocking the UI thread until a specific operation is complete.

Task and Task<T> for a CPU-Bound Operation

In the context of a CPU-Bound operation, because the work is done on the CPU, there’s no way to get around without dedicating a thread for the computation.

The use of async and await in the scenario, provides you with a clean way to interact with a background thread and keep the caller of the async method responsive.

Note that this does not provide any protection for shared data. If you are using shared data, you will still need to apply an appropriate synchronization strategy.

public async Task<int> CalculateResult(InputData data)
{
    // This queues up the work on the managed ThreadPool
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult method is still executing!

    // Executio of CalculateResult is yielded here to its caller!
    var result = await expensiveResultTask;

    return result;
}

The sequence of how this gets executed is as follows:

  • CalculateResult() executes on the thread it was called on. When it calls Task.Run, it queues the expensive CPU-bound operation, DoExpensiveCalculation(), on the thread pool and receives back a Task<int>.
  • DoExpensiveCalculation() is eventually running concurrently on the next available thread, likely on another CPU core. It’s possible to do concurrent work while DoExpensiveCalculation() is busy on another thread, because the thread which called CalculateResult() is still executing.
  • Once await is encountered, the execution of CalculateResult() is yielded to its caller, allowing other work to be done with the current thread while DoExpensiveCalculation() creates a result.
  • Once DoExpensiveCalculation() has finished, the remaining of the CalculateResult() method is queued up to run again, at which point it will have the result of DoExpensiveCalculation().

Practical I/O bound example

Before closing this 1st part of the async/await pattern in C#, let’s look at some code. We will have a look at an example I/O bound operation, leveraging the asynchronous programming model with the use of the async/await keywords in C#. We will use one simple console application for the sake of the example. Consider the following code snippet:

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace IOBoundOperation
{
    class Program
    {
        static async Task Main()
        {
            Console.WriteLine($"Thread {Environment.CurrentManagedThreadId} starts execution of {nameof(Main)}.");

            var resultTask = GetFirstCharactersAsync("http://www.dotnetfoundation.org", 10);

            for (var i = 0; i < 10; i++)
            {
                Console.WriteLine($"Doing random work on thread {Environment.CurrentManagedThreadId} in {nameof(Main)}.");
            }

            var result = await resultTask;

            Console.WriteLine($"Thread {Environment.CurrentManagedThreadId} result in {nameof(Main)}: {result}");

            Console.ReadLine();
        }

        private static async Task<string> GetFirstCharactersAsync(string url, int count)
        {
            Console.WriteLine($"Thread {Environment.CurrentManagedThreadId} starts execution of {nameof(GetFirstCharactersAsync)}.");

            // Execution is synchronous here
            using var client = new HttpClient();

            var pageTask = client.GetStringAsync(url);

            // Try to change here the value from 10 -> 10000 to see how the await pageTask
            // below will run synchronously as the Task will have already been completed.
            for (var i = 0; i < 10; i++)
            {
                Console.WriteLine($"Doing random work on thread {Environment.CurrentManagedThreadId} in {nameof(GetFirstCharactersAsync)}.");
            }

            // Execution of GetFirstCharactersAsync() is yielded to the caller here
            // GetStringAsync returns a Task<string>, which is *awaited*
            var page = await pageTask;

            Console.WriteLine($"Thread {Environment.CurrentManagedThreadId} resumes execution of {nameof(GetFirstCharactersAsync)} when the client.GetStringAsync task completes.");

            // Execution resumes when the client.GetStringAsync task completes,
            // becoming synchronous again.
            return count > page.Length ?
                page : page[..count];
        }
    }
}

In the above example, Thread 1 (let’s call it Main thread in our case) will start the execution of the Main method of our simple console program. It will first print a simple message in the console:

Console.WriteLine($"Thread {Environment.CurrentManagedThreadId} starts execution of {nameof(Main)}.");

It will then call GetFirstCharactersCountAsync method. This is an async method which will eventually make an I/O network call to some url address and fetch back the 10 first characters of the HTML document of this specific address (“http://www.dotnetfoundation.org").

Please note here, that although GetFirstCharactersCountAsync is an async method, the Main method is not yet awaiting it. Main method is just instructing the GetFirstCharactersCountAsync to start its work. As a result, Thread 1 will continue its execution (synchronously) and step into GetFirstCharactersCountAsync method. First, it will print a simple message in the console:

Console.WriteLine($"Thread {Environment.CurrentManagedThreadId} starts execution of {nameof(GetFirstCharactersAsync)}.");

Then it will instantiate an HttpClient object and will call the GetStringAsync method of it passing the url, from which we want to fetch the 10 first characters of. Again, this async call is triggered without actually Thread 1 awaiting it. So, Thread 1 will continue its execution synchronously. Then, it does a simple for loop for 10 times and then comes the magic. The code execution steps into this line:

var page = await pageTask;

Now, the GetFirstCharactersCountAsync() method is being suspended here, execution is yielded to the caller (Main method) and GetStringAsync returns a Task<string>, which is awaited.

The execution resumes inside Main method, where another for loop is being executed for 10 times.

After this for loop in Main we again step into the following line:

var result = await resultTask;

What do we have now? Execution of Main is also suspended here, the control of execution is yielded to the caller. Which is the caller of Main? We do not have actually one, so here the console app simply have to wait for all tasks to complete. But, keep in mind, that the thread is not actually blocking its execution. It is just waiting asynchronously.

An interesting thing to note also is that, in an ASP.NET web application context, the thread would return in the ThreadPool and would then be instantly ready to handle other http requests for all the time period the awaited tasks would need to last until they are complete and they had their respective results ready.

Back to our example, Main method is awaiting the resultTask of type Task<string> to complete. This is actually coming from GetFirstCharactersCountAsync async method, which in turn is awaiting for the HttpClient’s GetStringAsync method to also complete. The latter needs to happen first, in order for the program to do what it is supposed to do.

Eventually, the GetStringAsync method of the HttpClient object will complete and will return the first 10 characters of the url we have passed to GetFirstCharactersCountAsync method and will be stored inside the page variable.

When this happens, possibly another thread comes in, takes action and resumes code execution, which actually becomes synchronous again. Finally, the execution will arrive at this code block:

// Execution resumes when the client.GetStringAsync task completes,
// becoming synchronous again.
return count > page.Length ?
    page : page[..count];

The GetFirstCharactersCountAsync will return a result here, so code execution in Main can now actually continue after the line var result = await resultTask, until the code reaches the end of Main, that will mean the end of the console program.

Let’s now observe what the output of that code would be:

Console output
Console output

We can see here that Thread 1 (Main thread) started execution in Main, called GetFirstCharactersCountAsync, continued its execution inside that method, did some random work there (for loop) and encountered the first await inside GetFirstCharactersCountAsync.

The execution was yielded to the caller here (Main method), so Main got back control and Thread 1 continued with some random work here as well (for loop). Once the GetFirstCharactersCountAsync was also itself awaited inside Main method, Thread 1 actually suspended its execution and started waiting asynchronously.

After the HttpClient GetStringAsync method completed and returned a result back, Thread 5 came into play and resumed execution of GetFirstCharactersCountAsync after the await statement, returned some string as a result of the GetFirstCharactersCountAsync, which in turn was returned back as a result of the awaited operation in the line var result = await resultTask inside Main.

Finally, Thread 5 also continued execution after the await in Main and ended the console program execution.

You can find the code for the above example here.

Summary

In conclusion, this initial exploration into asynchronous programming with async and await in C# has laid the foundation for crafting efficient and clean code. We’ve demystified async methods, highlighted the roles of Task and Task<T>, and clarified misconceptions about thread creation.

As we progress through the series, we’ll delve deeper into advanced concepts and practical applications. Whether you’re developing responsive UIs or scalable server applications, mastering asynchronous programming in C# unlocks powerful capabilities. Cheers to harnessing the full potential of async and await for high-performance applications!

Написать сообщение
Тип
Почта
Имя
*Сообщение