Asynchronous Functions in C# - Async and Await
In my last tutorial on basics of threading, I explained what threading basically is and how it is actually implemented in C#. I would advise you to have a look at it before continuing with this article or if you have an Idea of what threads actually are, you can continue with this article. Writing a function and implementing a piece of code in it is not a big deal, anyone can write a function, pass it two variables add them and return back to the calling function. But in real world scenarios, where multiple tasks need to be performed simultaneously, this approach lacks sophistication and efficiency. This is why threading is so important because of threading comes with its ability to allow developers to write concurrent function which is more advanced form of functions which foster concurrency,
Before, dwelling into details of asynchronous functions, let us first differentiate between the synchronous and asynchronous functions. In simplest words, synchronous functions block the calling function until they complete their task. For instance, Thread.Sleep, Console.WriteLine and Console.ReadLine, these function do not let the control to shift to the next lines until they perform their functionality. Asynchronous methods are those methods which return immediately to the caller function and continue executing their task in parallel with the caller function. They do not block the caller function and it is due to this reason that they are called non-blocking functions.
Traditionally concurrency is implemented via creating a function outside the body of caller’s function and then passing this function as a delegate to a thread or task in order to achieve concurrency. Concurrency can be achieved this way but it comes with some overheads. In contrast to implementing concurrency via threads where function that has to be run parallel was initiated from outside of the calling function, asynchronous functions initiate concurrency from within the function instead of handing this responsibility over to threads or tasks.
Suppose there is a method that calculates the sum of squares of the range of numbers passed to it and then displays it. Calculating sum of squares of range of number can be a long task. Therefore, it can be divided into two functions. One function that calculates the sum of squares of range of numbers and the other function that displays this result. Still, major portion of time would be spent on calculating sums rather than displaying. Traditionally in order to display the sums concurrently, we create a new thread or task and pass a delegate to a method that displays the sum. This method would in turn call the method that calculates the actual sum of range of variable. This is the concept of course-grained concurrency. In our first example, we explain this concept.
In the main method, we have created a newtask and have passed it the DisplaySumOfSquaresOfRange method which would in turn call GetSumOfSquaresOfRange method. These chains of methods are called graphs programming terms. Both of these two functions will run on a new task or pooled thread. But main functionality is only being perfomed by GetSumOfSquaresOfRange method. This is called course-grained concurrency where all the chained methods are on a separate thread but most of them do not perform long tasks. In rich client applications such as WPF and Windows Forms, the course-grained concurrency can result in less responsive UI since there are multiple functions chained on one thread or task. The output of the code in Example1 is as follows:
Note: The output is huge; therefore we have printed some portion of it to demonstrate the concept.
In console applications, course-grained concurrency doesn’t have much impact on the performance since user interaction and need for responsiveness is minimal. However, in rich client application, course-grained concurrency can hinder the performance and responsiveness of the UI. In such cases, fine-grained concurrency is a more efficient solution. In the next example of the tutorial, Example1 has been modified to demonstrate how fine-grained concurrency is implemented in applications. Have a look at the second example of this tutorial.
The first modification is made in all those functions that take long time. In Example2, GetSumOfSquaresOfRange takes longer time for execution, therefore we have to append keyword Task, followed by the type of data which this method returns, Task<int> in case of GetSumOfSquaresOfRange. Inside the method, everything has to be enclosed inside the Task.Run method. This is called fine-grained concurrency because not all the methods on the chain will run in a separate thread but only those which take longer time. This makes rich client application more responsive and efficient.
The second modification is required to be made in all those functions which call this GetSumOfSquaresOfRange method. Calling functions should be made async by appending async keyword before the method declaration as we did for the DisplaySumOfSquaresOfRange method which calls GetSumOfSquaresOfRange method. We did this in function declaration as follows:
You saw that we used ‘await’ and ‘async’ keywords for fine-grained concurrency, let see what these functions actually does. If you have read the article on ‘Understanding threads and tasks’ you would remember that we used the following expression in the 8th Example of that tutorial:
The new “await” and “async keywords do exactly the same job but in a more cleaner and efficient. Though, we have implemented ‘await’ and ‘async’ keywords in Example2, but in our next example, we would perform Division as we did in the above code snippet, but this time, using ‘await’ and ‘async’. Have a look at the code 3rd Example:
Secondly, ‘await’ can only be called inside those methods that have been specified ‘async’ as DisplayDivision method in Example3. The output of the code in Example3 would be as follows:
The methods which have ‘async’ modifier associated with them are typically called asynchronous methods. It is due to the fact that when a compiler reaches ‘await’ keyword, what happens is that control is shifted back to the method in which ‘await’ is present but before giving control back to the calling method, a continuation is attached with the task (await is always on a task). When task completes, the control is again shifted back to the continuation which is executed.
Best thing about keyword ‘await’ is that it can be used anywhere inside the method which has modified as ‘async’. It can be used inside loops, with lambda expressions, delegates or anywhere you want except for the catch, finally, unsafe and lock blocks.
A very good approach is to make async methods, themselves awaitable. In fact all the methods in a method chain should be made awaitable except the method which is called from the main method. To make a method both asynchronous and awaitable, you have to use async modifier and the next change is the return type of the method which should be Task. The new method can be both awaited also, ‘await’ can be used inside the method. The 4th example of this tutorial demonstrates this concept.
|All times are GMT +5.5. The time now is 12:21.|