Introduction to Asynchronous Programming
Asynchronous programming is a powerful feature of C# that allows developers to write code that can perform tasks in the background, without blocking the execution of the rest of the application. In this article, we will learn the details of Asynchronous Programming and how to implement it in C#.NET.
Asynchronous programming can be very useful whenever you have any I/O-bound requirements or have requirements to run long CPU-Bound operations.
Some of the scenarios where Asynchronous Programming can be used are -
-
Requesting data from the network.
-
Accessing data from the database.
-
Reading and writing files to the system.
-
Perform Expensive Calculations.
Asynchronous programming can be very useful to run such long tasks without blocking the main thread of the application.
C# provides inbuilt support for Asynchronous programming using Asynchronous Programming Model, which makes it simple to write asynchronous code. It uses the Task-based Asynchronous Pattern (TAP).
Advantages of Asynchronous Programming
Before we understand how to implement Asynchronous Programming in C#.NET, let's try to understand its advantages of it.
-
Improved responsiveness - Asynchronous programming allows your application to continue running while waiting for long-running tasks to complete. This can result in a more responsive and user-friendly application.
-
Increased scalability: Asynchronous programming allows your application to handle multiple tasks at the same time, rather than waiting for one task to complete before starting the next one. This can result in better resource utilization and increased scalability.
-
Better throughput: Asynchronous programming can improve the throughput of your application by allowing it to handle more tasks in parallel. This can result in faster performance and better utilization of available resources.
-
Simplified code: The async and await keywords in C# make it easy to write asynchronous code in a synchronous manner. This simplifies your code and easier to understand.
-
Better Utilization of Multi-Core processors: Asynchronous programming allows better utilization of multi-core processors. This increases the overall performance of the application.
-
Reduced Latency: Asynchronous programming helps reduce the latency of the code since the operations execute in parallel.
Async and Await - Pillars of Asynchronous Programming in C#.NET
In C#, the core foundation of Asynchronous programming is based on the Task
and Task<T>
objects. Along with this, the two key keywords,async
and await
are used to run the async
tasks. The async
keyword is used to define a method as asynchronous, while the await
keyword is used to indicate that the method is waiting for a task to complete.
Now, we will look at some interesting examples and scenarios to understand more about asynchronous programming.
Example 1 - Downloading data from Network.
For example, consider the following method that retrieves data from a web service.
public string GetData()
{
var webClient = new WebClient();
return webClient.DownloadString("http://example.com/data");
}
As you can see, we have written the above method in a synchronous way. The method is responsible to download the data over the internet which can be a long operation. Hence, Running this method in a synchronous can block the main thread of the application.
However, if we update this method to use the async
and await
keywords, we can make this method asynchronous.
public async Task<string> GetDataAsync()
{
var webClient = new WebClient();
return await webClient.DownloadStringTaskAsync("http://example.com/data");
}
In this example, the GetDataAsync
method is asynchronous and returns an Task
object, while the await keyword is used to indicate that the method is waiting for the DownloadStringTaskAsync
task to complete. This will make the method run asynchronously allowing the rest of the application to continue running. Using Task, async, and await, the data in this case will be downloaded in the background.
Example 2 - Running Multiple Tasks in parallel
You may find yourself in a situation where you can retrieve multiple data parallelly. To achieve this, the Task API of .NET provides two methods, Task.WhenAll
and Task.WhenAny
, which allows you to write asynchronous code that performs a non-blocking wait on multiple background jobs. Let's understand this with an example.
Let's consider a scenario where you want to download the data for multiple stocks over the internet.
First, let's look at the code implemented without using an asynchronous programming model.
public Stock GetStockDetail(string symbol)
{
// Code omitted:
// Given the stock symbol, this will download the data for the stock over the internet.
}
public IEnumerable<Stock> GetStockDetails(IEnumerable<string> stockSymbols)
{
var result = new List<Stock>();
foreach (string symbol in stockSymbols)
{
result.Add(GetStockDetail(symbol));
}
return result;
}
So, what is happening here? As you can see, the function GetStockDetail
retrieves the stock data for 1 stock symbol. Consider this function downloads data over the network and takes around 5 secs to complete. Now the main function GetStockDetails
which receives the list of stock symbols as a parameter, calls the function GetStockDetail to download the data. With synchronous programming, we used the for each loop and downloaded the data one by one on the main thread. This will wait for each symbol data to be downloaded and will block the execution of the Main Thread.
Now, let's check how we can efficiently improve the code performance for the above code using asynchronous programming.
public async Task<Stock> GetStockDetailAsync(string symbol)
{
// Code omitted:
// Given the stock symbol, this will download the data for the stock over the internet.
}
public async Task<IEnumerable<Stock>> GetStockDetailsAsync(IEnumerable<string> stockSymbols)
{
var result = new List<Task<Stock>>();
foreach (string symbol in stockSymbols)
{
result.Add(GetStockDetailAsync(symbol));
}
return await Task.WhenAll(result);
}
As you can see, we have updated the function GetStockDetail to use the async keyword which makes this function asynchronous in nature. We have also converted the function GetStockDetails to use the async keyword and it returns the Task.
Also, if you observe the code closely, in the for each loop, we have not used await keyword on the GetStockDetail. We have just called the function by passing the symbol and we have saved the Task object return by it in the result list. With this, it will create multiple parallel background jobs for each symbol.
In the end, we wait for all the tasks to complete before returning the result using await Task.WhenAll(result)
statement.
With this approach, this will not block the main thread and also we will achieve parallelism to download multiple stock data concurrently.
Exception Handling in Asynchronous Programming
Exception handling is an important aspect of programming, and it is no different in asynchronous functions in C#.NET. Exceptions can occur in asynchronous code just as they can in synchronous code, and it is important to have a proper strategy in place to handle them.
In C#, try-catch blocks are used to handle exceptions. The same is true for asynchronous functions, but there are some additional considerations to keep in mind.
First, it is important to understand that an exception thrown in an asynchronous function will be propagated through the Task or Task object that is returned by the function. This means that you can use the await keyword to catch the exception and handle it in the same way that you would handle an exception thrown in a synchronous function.
For example:
public async Task DoWorkAsync()
{
try
{
// code that may throw an exception
}
catch (Exception ex)
{
// exception handling code
}
}
It is also worth noting that when an exception is thrown in an asynchronous function, it will be propagated through any continuations that are attached to the task. This means that if you have multiple await statements in your function, each one will have the opportunity to catch the exception.
Another way to handle exceptions in the async function is by using the ContinueWith
method, which is a method of the Task API that allows you to specify a callback that will be executed when the task completes. You can use this method to catch exceptions that occur in an asynchronous function.
public async Task DoWorkAsync()
{
var task = DoWorkThatMightThrow();
task.ContinueWith(t =>
{
if (t.IsFaulted)
{
// Handle exception
}
});
}
Tips and Advice
There are some things you should keep in mind before using asynchronous programming to avoid any unexpected behavior in the program.
1. Async
methods must have await
keyword in the body
If await
is not used in the body of async
method, the C# compiler will give a warning, but the code will still compile and C# will treat it as a normal synchronous block of code.
2. Follow Naming Convention to Add Async as a suffix to every async method
Using this naming convention is important, as it makes it easier to differentiate between the synchronous and asynchronous implementation of the same functionality.
3. Be Careful while using async Lambdas with LINQ Operations
Lambda expressions in LINQ use deferred execution, which means, the code can execute when you are not expecting it to execute it. If not written properly, the addition of blocking jobs in the lambda expressions can quickly lead to a deadlock. Async and LINQ are very powerful together but should be used with care. You can read more on LINQ in this blog post.
4. Use async void
Only with Events
Since Events in C# do not have return types, using async void
is the only way to allow running asynchronous Events. Hence, using async void other can events can lead to the below problems -
-
If Exceptions are thrown in
async void
methods, they cannot be caught outside of that method. -
If
async void
is used in a method, it becomes difficult to test it. -
It can cause bad side effects if the caller isn't expecting them to be async.
Conclusion
In conclusion, asynchronous programming in C# allows developers to create more responsive and efficient applications by offloading long-running tasks to the background. The async and await keywords make it easy to write asynchronous code in a synchronous manner, allowing developers to focus on the logic of their application, rather than the intricacies of multithreading.
We also learned about Exception Handling in async
functions. It is important to handle exceptions in asynchronous functions to ensure that your application continues to function as expected. By using try-catch blocks and the await keyword, you can catch and handle exceptions just as you would in synchronous code. Additionally, the ContinueWith method can be used to catch exceptions that occur in an asynchronous function.
Thank you for taking the time to read this blog post. We hope that you found the information provided in the blog to be helpful and informative. For similar content, please check out our other blogs.
We appreciate your support and feedback, and we would love to hear from you if you have any questions or comments about the blog. If you have any specific topic you want us to cover in the future, please feel free to let us know.
Once again, thank you for reading our blog, and we look forward to your continued support.