Asynchronous Programming in .NET Core
By FoxLearn 12/19/2024 4:11:41 AM 31
Through code examples, readers will learn how to implement asynchronous methods in C# and understand when to use them effectively.
Mastering Asynchronous Programming in .NET Core C#
If you're new to asynchronous programming, this article is perfect for learning about async programming in .NET Core C#. C# supports async programming, and in this article, we focus on two key keywords: async
and await
. You'll gain a detailed understanding of how these keywords work and how to implement them in your C# applications.
Asynchronous vs Synchronous
To understand the difference between synchronous and asynchronous processes, let's consider a real-world example from a restaurant. Imagine two guests arrive at the restaurant and sit at separate tables during a quiet time, with only one attendant available to serve them. The process of serving the guests can help illustrate the concepts of sync and async programming, showing how each approach handles tasks and waits for completion.
Synchronous
In synchronous mode, tasks are executed one by one, with each task waiting for the current one to complete before moving on to the next. For example, the restaurant attendant first attends to the guests at the 1st table, takes their order, and then goes to the kitchen to place it. The attendant waits in the kitchen until the food is ready, serves it, waits for the guests to finish their meal, serves the bill, and collects payment. Only after completing all these tasks for the first table does the attendant proceed to serve the second table.
In the synchronous process, each activity is blocked until the current one is completed, even if waiting isn't necessary. For example, while the food for the first guest is being prepared, the attendant waits in the kitchen despite having no involvement in that step. Additionally, there are independent tasks, like taking orders from the second table, that could happen in parallel. This waiting and blocking of activities causes delays, ultimately increasing the total time required to complete the entire process.
Asynchronous
In asynchronous mode, tasks can run in the background and notify you when they are completed, allowing the next task to begin before the previous one finishes. For example, the attendant takes orders and places them in the kitchen for both tables without waiting. While waiting for the food to be prepared, the attendant can serve the table whose food is ready first, and similarly, serve the bill or collect payment from guests who are ready. This parallel processing eliminates delays, as the attendant continuously moves between tasks without unnecessary waiting or blocking, ensuring a more efficient workflow.
In programming, threads act like the attendants, and tasks are similar to lines of code. Long-running tasks, such as reading files, making HTTP calls, or performing heavy computations, can block the application when executed synchronously. This blocking prevents the application from performing other tasks and can overwhelm web applications, as the number of available threads is limited. If all threads are busy, new requests must wait in the queue.
Asynchronous programming addresses this by allowing tasks to run in parallel. It also enables parallelism within a task itself. For example, if a guest orders multiple food items, these can be prepared in parallel rather than sequentially. However, not all tasks can run in parallel; if one task depends on the result of another, they must be executed in sequence.
Getting Started with Asynchronous Programming
Asynchronous programming allows tasks to run in the background without blocking the main thread, enabling the execution of multiple independent tasks in parallel. This non-blocking approach reduces delays caused by waiting and improves efficiency. By breaking sequential code into logical blocks that can run concurrently, async programming enhances the scalability of applications, particularly in web environments like IIS, where threads can handle multiple requests simultaneously. However, not all tasks can be parallelized, especially if one task depends on the result of another. Beyond performance, asynchronous programming also improves the overall user experience by making applications more responsive.
In .NET Core C#, asynchronous programming is implemented using the async
and await
keywords. To make a method asynchronous, the async
keyword is added before the return type in the method definition.
Async Return Types
In asynchronous programming, the void
return type is used for event handlers that require a void
return type. However, for async methods that don’t return a value, it is recommended to use Task
instead of void
. This is because async methods returning void
cannot be awaited, making them "fire and forget" methods. Additionally, the caller of an async method that returns void
cannot catch any exceptions thrown within the method.
public async void SaveChangesAsync() { //Save your data }
The Task
return type is used for asynchronous methods that don’t return a value or return something that isn't an operand. If the method were synchronous, it would return void
. Using Task
as the return type allows the caller to await
the method, suspending the caller's execution until the asynchronous method has completed. This enables better control and handling of asynchronous operations.
public async Task SaveChangesAsync() { //Save your data }
The Task<TResult>
return type is used for asynchronous methods that return a value (an operand). This allows the caller to await
the method's response, suspending the caller's execution until the asynchronous method has completed and the result is available. This ensures proper handling of asynchronous operations that need to return a specific result.
public async Task<int> SaveChangesAsync() { //Save your data return 1; }
Starting with C# 7.0, the ValueTask<TResult>
return type was introduced to improve performance in asynchronous programming. It allows async methods to return any type with an accessible GetAwaiter
method that provides an awaiter instance, as long as the type is decorated with the AsyncMethodBuilderAttribute
. Unlike Task
and Task<TResult>
, which are reference types and can lead to memory allocations that impact performance in tight loops, ValueTask<TResult>
is a value type that reduces memory overhead, offering better performance in scenarios where high-frequency asynchronous operations are needed.
public async ValueTask<int> SaveChangesAsync() { //Save your data return 1; }
Starting with C# 8.0, an async method can return an async stream using the IAsyncEnumerable<T>
interface. This allows for enumerating items from a stream where elements are generated in chunks through repeated asynchronous calls. IAsyncEnumerable<T>
enables efficient handling of large datasets or continuous streams, as it allows processing each item as it becomes available without blocking the thread.
Benefits of Asynchronous Programming:
UI is Not Blocked – By executing long-running tasks in the background, the UI remains responsive, allowing users to interact with the application and perform other operations without delays.
Performance Improvement – Asynchronous programming enhances performance by enabling parallel execution of independent tasks. Running multiple tasks in parallel reduces the total time required to complete activities.
Improves Vertical Scalability – In web applications, where the number of threads in the pool is limited, asynchronous tasks prevent threads from being blocked. Instead of waiting for a task to complete, the thread can await its completion, improving the application's ability to handle multiple requests concurrently.
While asynchronous programming offers clear benefits, it also introduces added complexity during application development. It can make applications more difficult to enhance or modify due to the intricate structure of parallel tasks. Debugging and identifying bugs in asynchronous applications is also more challenging, as multiple tasks run concurrently. Additionally, the increased number of objects in memory sometimes held longer until all tasks are completed can lead to higher memory usage and potential performance concerns.
Asynchronous programming is ideal for handling long-running, blocking tasks. When a piece of code can run independently of the rest of the process, it can be executed as an async task on a separate thread, allowing the main thread to remain free and keep the application responsive. Examples of long-running tasks include reading or writing files, making HTTP calls to external APIs, performing database calls over a network, conducting heavy computations, or writing application logs to a file. Using async for these tasks helps prevent the main thread from being blocked, ensuring a smoother user experience.
Implementing Asynchronous Programming in C# .NET Core
Here's a similar example that demonstrates how asynchronous programming works in C# .NET Core using async and await
using System; using System.Diagnostics; using System.Threading.Tasks; public class Program { static readonly Stopwatch timer = new Stopwatch(); public static async Task Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); // Calling async methods using await await Task1Async(); await Task2Async(); Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); } // Async method 1 private static async Task Task1Async() { Console.WriteLine("Async Task 1 Started - " + timer.Elapsed.TotalSeconds.ToString()); await Task.Delay(2000); // Simulating a long-running task Console.WriteLine("Async Task 1 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } // Async method 2 private static async Task Task2Async() { Console.WriteLine("Async Task 2 Started - " + timer.Elapsed.TotalSeconds.ToString()); await Task.Delay(3000); // Simulating a long-running task Console.WriteLine("Async Task 2 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } // Synchronous method 1 (for comparison) private static void Task1() { Console.WriteLine("Task 1 Started - " + timer.Elapsed.TotalSeconds.ToString()); Thread.Sleep(2000); // Simulating a long-running task Console.WriteLine("Task 1 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } // Synchronous method 2 (for comparison) private static void Task2() { Console.WriteLine("Task 2 Started - " + timer.Elapsed.TotalSeconds.ToString()); Thread.Sleep(3000); // Simulating a long-running task Console.WriteLine("Task 2 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } }
Output:
Program Start - 00:00:00.0000014 Async Task 1 Started - 0.0022814 Async Task 1 Completed - 2.0533916 Async Task 2 Started - 2.054363 Async Task 2 Completed - 5.055162 Program End - 00:00:05.0556649
The Task1Async
& Task2Async
methods use async
and await
. They simulate a delay using Task.Delay
, allowing other operations to run concurrently while waiting for the task to complete.
The Task1
& Task2
methods use Thread.Sleep
, which blocks the current thread until the task is completed, delaying the program execution.
async – The async
keyword is used in a function definition (before the function name) to mark the function as asynchronous, allowing it to run asynchronously.
await – The await
keyword is used to call an async function asynchronously. It is placed before the function call to pause the execution of the calling method until the async function completes.
In real applications, when tasks can run independently, you want to start them immediately to execute in parallel. Running tasks in parallel reduces the overall execution time compared to running them sequentially. To run tasks in parallel, you start each task and hold onto the Task
object (from System.Threading.Tasks.Task
) representing the work. Each task must be awaited before progressing toward program completion to ensure all tasks finish before the program ends.
public static async Task Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); // Calling async methods using await Task task1Task = Task1Async(); Task task2Task = Task2Async(); await task1Task; await task2Task; Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); }
Output:
Program Start - 00:00:00.0000015 Async Task 1 Started - 0.0025218 Async Task 2 Started - 0.0157975 Async Task 1 Completed - 2.0312625 Async Task 2 Completed - 3.0293604 Program End - 00:00:03.0298515
In the above code, we store the tasks (task1Task
and task2Task
) representing operations (Task1Async
and Task2Async
) as they start. The tasks are not awaited immediately upon starting. After both tasks are initiated, we then await the task objects (task1Task
and task2Task
) before completing the Main
method, ensuring that both tasks are fully completed before the program finishes.
You can await multiple tasks, which results in several await
statements. If there are many tasks, this leads to a series of await
statements in the code. This can be improved by using methods available in the Task
class, such as Task.WhenAll
, which allows you to wait for multiple tasks concurrently, making the code more concise and efficient.
public static async Task Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); // Calling async methods using await Task task1Task = Task1Async(); Task task2Task = Task2Async(); await Task.WhenAll(task1Task, task2Task); Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); }
Output:
Program Start - 00:00:00.0000014 Async Task 1 Started - 0.0023352 Async Task 2 Started - 0.0148163 Async Task 1 Completed - 2.026615 Async Task 2 Completed - 3.0203484 Program End - 00:00:03.0285613
In the above code, we have modified it to use the WhenAll
method from the System.Threading.Tasks.Task
class, which returns a Task
that completes when all the tasks in its argument list have finished.
Another available option is the WhenAny
method from the System.Threading.Tasks.Task
class. This method returns a Task<Task>
that completes when any of its tasks completes. You can use WhenAny
in scenarios where you want to wait until at least one task finishes.
- How to use BlockingCollection in C#
- Calculating the Distance Between Two Coordinates in C#
- Could Not Find an Implementation of the Query Pattern
- Fixing Invalid Parameter Type in Attribute Constructor
- Objects added to a BindingSource’s list must all be of the same type
- How to use dictionary with tuples in C#
- How to convert a dictionary to a list in C#
- Dictionary with multiple values per key in C#