A Simplified Guide to Async/Await in C# Programming
As a developer, understanding the difference between synchronous and asynchronous programming is crucial to effectively utilize the benefits of asynchronous programming in C#.

What is Synchronous Programming?

  • Synchronous programming refers to the traditional approach where operations are executed sequentially, one after another. In this paradigm, each operation must complete before the next one can start.
  • For example, imagine a scenario where a web application needs to fetch data from a remote server. In synchronous programming, the application would send a request to the server and then wait for a response. During this waiting period, the program execution halts, blocking the main thread.

What is Asynchronous Programming?

  • Asynchronous programming allows multiple operations to run concurrently without blocking the main thread. Instead of waiting for each operation to complete, the application can continue with other tasks. This non-blocking behavior greatly improves performance and responsiveness.
  • For instance, in the previous example of fetching data from a remote server, an asynchronous approach would allow the application to send the request and continue executing other tasks while waiting for a response. This gives developers the freedom to perform additional operations or respond to user interactions.

Basics of Async and Await in C#

  • Async/await is a powerful feature in C# that allows developers to write asynchronous code in a more synchronous and intuitive manner. It provides a way to execute long-running operations without blocking the main thread, resulting in more responsive and efficient applications.

Understanding the "async" Keyword

  • The “async” keyword is used to define methods that can execute asynchronously. By marking a method as “async,” it becomes eligible for asynchronous behavior. The “async” keyword does not inherently make a method execute asynchronously, but it enables the usage of the “await” keyword within the method body.
  • For example, consider the following method declaration:
public async Task<string> FetchDataAsync()
{
    // Asynchronous operations...
  await Task.Delay(1000); // Simulating an asynchronous delay
    return "Data fetched successfully!";
}

In this example, the “async” keyword is used to define the method “FetchDataAsync” as an asynchronous method. The method body contains an “await” expression, which we’ll explore in the next section.

Understanding the "await" Keyword

  • The “await” keyword is used within an “async” method to indicate that the method should pause its execution until the awaited asynchronous operation is complete. While waiting, the thread is freed to perform other tasks, ensuring the application remains responsive.
  • Continuing with the previous example, let’s take a closer look at the “await” keyword:
public async Task<string> FetchDataAsync()
{
    // Asynchronous operations...
  await Task.Delay(1000); // Simulating an asynchronous delay
    return "Data fetched successfully!";
}
  • In this example, the “await” keyword is used to pause the execution of the “FetchDataAsync” method until the completion of the asynchronous delay operation. Once the delay is over, the method will resume its execution, and the result “Data fetched successfully!” will be returned.
Example of Async and Await
public class Program
{
    	static void Main(string[] args)
        	{
				Console.WriteLine("Welcome to C#");
            	GetMethod();
            	Console.WriteLine("After Get Method Code");
            	Console.ReadKey();
        	}
    	// Declare an async method
    	public async static void GetMethod()
        	{
           	Console.WriteLine("C# Tutorial Start");
           	// Use the await keyword
           	await Task.Delay(TimeSpan.FromSeconds(10));
           	Console.WriteLine("\n");
           	Console.WriteLine("End Get Method");
         	}
}


Error Handling and Exception Handling in Async and Await

  • While asynchronous programming offers numerous benefits, it also introduces challenges in error handling and exception management. It’s crucial to understand how to handle errors effectively in asynchronous operations.

Error Handling and Exception Handling in Asynchronous Operations

  • When dealing with asynchronous methods, error handling techniques should be implemented to handle exceptional situations. This can be accomplished using try-catch blocks within async methods.
  • By wrapping the asynchronous operations in a try-catch block, developers can catch and handle exceptions that might occur during execution.
Example
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
	static async Task Main()
	{
	try
    	{
        	// Start an asynchronous operation
        	string apiResponse = await MakeApiRequestAsync("https://jsonplaceholder.typicode.com/post/1");
        	Console.WriteLine($"API Response: {apiResponse}");
    	}
    	catch (Exception ex)
    	{
        	// Handle exceptions thrown during the asynchronous operation
        	Console.WriteLine($"An error occurred: {ex.Message}");
    	}
	}
	static async Task MakeApiRequestAsync(string apiUrl)
	{
   	try
   	{
      	using (var httpClient = new HttpClient())
       	{
            	// Making an asynchronous GET request
      	  HttpResponseMessage response = await httpClient.GetAsync(apiUrl);
// Ensure the response was successful before proceeding
                response.EnsureSuccessStatusCode();
 
            	// Read the response content as a string
            	return await response.Content.ReadAsStringAsync();
        	}
    	}
    	catch (HttpRequestException httpEx)
    	{
        	// Handle HTTP-related exceptions
        	Console.WriteLine($"HTTP Error: {httpEx.Message}");
        	throw; // Re-throw the exception if necessary
    	}
	}
}

While handling exceptions in asynchronous programming, it’s important to consider that exceptions thrown within an async method are not propagated directly to the caller. Instead, they are captured and stored within the returned Task object.

When to use Async and Await in C#?

These features offer a way to perform non-blocking operations, enhancing the responsiveness and scalability of applications.
1. Web Request:
  • When making HTTP requests using ‘async’ and ‘await’ allows your application to remain responsive while waiting for those responses.
  • Example: – Get Response from server
public async Task<string> GetApiResponseAsync(string apiUrl)
{
   using (var httpClient = new HttpClient())
{
     HttpResponseMessage response = await httpClient.GetAsync(apiUrl);
 	response.EnsureSuccessStatusCode();
 	return await response.Content.ReadAsStringAsync();
}
}
2. Database Access:
  • When querying or updating a database, use async and await to avoid blocking the main thread and allow the application to handle other tasks concurrently.
  • Example: Querying a database asynchronously.
public async Task<User> GetUser(int id)
  {
   	using (var dbContext = new MyDbContext())
{
   	return await dbContext.Users.FindAsync(userId);
  }
}
3. I/O Operations:
  • Use async and await when performing I/O-bound operations, such as reading from or writing to files, making HTTP requests, or querying a database.
  • Example:- Reading data from a file asynchronously.
public async Task<string> ReadFileAsync(string filePath)
{
using (var reader = new StreamReader(filePath))
{
       return await reader.ReadToEndAsync();
	}
}
4. Parallel Execution:
  • Use async and await for parallel execution of independent tasks. This can improve performance by allowing tasks to run concurrently.
  • Example: Processing data concurrently.
public async Task<IEnumerable<string>> 
ProcessDataConcurrentlyAsync(IEnumerable<string> data)
{
      var tasks = data.Select(async item => await ProcessItemAsync(item));
      return await Task.WhenAll(tasks);
}

Best Practices for Using Async and Await in C#

To ensure efficient and effective usage of async and await in C# programming, developers should adhere to certain best practices. These best practices help avoid potential issues and enhance the overall performance and maintainability of the code.

Avoiding Deadlocks and Performance Issues
  •  In asynchronous programming, deadlocks can occur if improper synchronization or blocking operations are used. To prevent deadlocks, developers must be aware of  potential deadlock scenarios and employ appropriate techniques such as using Configure Await(false) or avoiding unnecessary blocking operations.
  • Using: To avoid capturing the context and resuming execution on the same context, use Configure Await(false) when possible
public async Task<List<User>> GetUser()
{
	using (var context = new MyDbContext())
	{
    	return await context.Products.ToListAsync().ConfigureAwait(false);
	}
}
  • Using for CPU-bound operations: Offload CPU-bound operations that can benefit from parallelism to a separate thread using Task.Run:
public async Task<int> CalculateResultAsync(int input)
{
	int result = await Task.Run(() => PerformComplexCalculation(input));
	return result;}
  • Additionally, optimizing performance in asynchronous programming can be achieved by minimizing unnecessary context switches, reducing unnecessary allocations, and utilizing asynchronous alternatives to synchronous APIs whenever possible.

Summary

In this article, we delved into the world of asynchronous programming in C#. We discussed the differences between synchronous and asynchronous programming, explored the basics of async and await keywords, examined error handling and exception management in async and await scenarios, and provided best practices for utilizing async and await effectively.

Shivani Kaniya