Post

Asynchronous Programming in C# with async/await

Asynchronous programming is a powerful technique that allows you to write more responsive, scalable, and performant applications. In this blog post, you will learn how to use the async and await keywords in C# to handle asynchronous tasks with ease.

Understanding Asynchronous Programming

Asynchronous programming is a way of writing code that does not block the current thread of execution until the task is completed. Instead, it allows the thread to continue with other work while the task runs in the background. This way, you can avoid wasting resources and improve the user experience.

Asynchronous programming is especially useful for scenarios where you need to perform tasks that are I/O-bound or CPU-bound. I/O-bound tasks are those that involve reading or writing data from external sources, such as files, databases, or web services. CPU-bound tasks are those that require intensive computations, such as image processing or encryption.

Synchronous programming, on the other hand, means that the code executes sequentially, blocking the current thread until the task is finished. This can lead to performance issues and unresponsive applications.

The Basics of async and await

To write asynchronous code in C#, you can use the async and await keywords. These keywords are part of the Task-based Asynchronous Pattern (TAP) that was introduced in C# 5.0 and .NET Framework 4.5.

The async keyword is used to mark a method as asynchronous. It indicates that the method can contain one or more await expressions, which are used to await the completion of an asynchronous task. An async method returns a Task or a Task<T> object, which represents the ongoing operation.

The await keyword is used to suspend the execution of an async method until the awaited task is completed. It also unwraps the result of the task if it has one. The await keyword can only be used inside an async method.

Here is a simple example of an async method that downloads a web page and returns its content as a string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Net.Http;
using System.Threading.Tasks;

public async Task<string> DownloadPageAsync(string url)
{
    // Create an HttpClient object
    using var client = new HttpClient();

    // Await the response from the web server
    var response = await client.GetAsync(url);

    // Await the content of the response
    var content = await response.Content.ReadAsStringAsync();

    // Return the content as a string
    return content;
}

By using an async method, you can improve the responsiveness of your application. For example, if you call this method from a UI thread, it will not block the UI while downloading the web page. Instead, it will return control to the UI thread and resume when the task is completed.

Creating and Using async Methods

To create and use an async method, you need to follow these steps:

  1. Add the async modifier to the method signature.
  2. Specify a return type of Task or Task<T>, where T is the type of the result (if any).
  3. Use the await keyword to await one or more asynchronous tasks inside the method body.
  4. Optionally, use the return keyword to return a result (if any).
  5. Call the async method from another method and await its result (if any).

It is recommended to follow some naming conventions for async methods:

  • Use an “Async” suffix for all async methods.
  • Use a “Task” suffix for methods that return a generic Task<T> object.
  • Use an “AsyncTask” suffix for methods that return a non-generic Task object.

Here are some examples of creating and using async methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// An async method that returns a Task<int>
public async Task<int> AddAsync(int x, int y)
{
    // Simulate some work
    await Task.Delay(1000);

    // Return the result
    return x + y;
}

// An async method that returns a Task
public async Task DoSomethingAsync()
{
    // Simulate some work
    await Task.Delay(1000);

    // Do something else
    Console.WriteLine("Done");
}

// An async method that calls async methods
public async Task Run()
{
    // Call an async method and await its result
    var sum = await AddAsync(3, 4);

    // Print the result
    Console.WriteLine($"Sum: {sum}");

    // Call another async method without awaiting its result (fire-and-forget)
    DoSomethingAsync();
}

Handling Multiple Asynchronous Tasks

Sometimes, you may need to work with multiple asynchronous tasks concurrently. For example, you may want to download several web pages at the same time, or perform some calculations in parallel.

To handle multiple asynchronous tasks, you can use the Task.WhenAll and Task.WhenAny methods. These methods are static methods of the Task class that return a Task object that represents the completion of a collection of tasks.

The Task.WhenAll method returns a Task that completes when all of the tasks in the collection have completed. It also returns an array of results from the tasks, if they have any. You can use this method to await multiple tasks and process their results together.

The Task.WhenAny method returns a Task that completes when any of the tasks in the collection have completed. It also returns the first task that completed, regardless of its status. You can use this method to await the fastest task and handle its result or exception.

Here is an example of using these methods to download multiple web pages concurrently:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System.Net.Http;
using System.Threading.Tasks;

public async Task DownloadPagesAsync(string[] urls)
{
    // Create an HttpClient object
    using var client = new HttpClient();

    // Create a list of tasks to download each web page
    var tasks = new List<Task<string>>();

    // Loop through the urls
    foreach (var url in urls)
    {
        // Start a task to download the web page and add it to the list
        var task = client.GetStringAsync(url);
        tasks.Add(task);
    }

    // Await all the tasks to complete and get their results
    var contents = await Task.WhenAll(tasks);

    // Loop through the contents
    foreach (var content in contents)
    {
        // Do something with the content
        Console.WriteLine(content);
    }
}

Exception Handling in Asynchronous Code

Exceptions are inevitable in any code, and asynchronous code is no exception. However, handling exceptions in asynchronous code can be tricky, as they may occur at different points in time and on different threads.

There are some best practices for error handling in async methods:

  • Use try/catch blocks inside async methods to catch exceptions that occur within them.
  • Use await to propagate exceptions from awaited tasks to the caller.
  • Use Task.IsFaulted, Task.Exception, and Task.Status properties to inspect the status and exception of a task.
  • Use Task.ContinueWith method to attach a continuation task that runs when a task completes, regardless of its status.
  • Use Task.WhenAll and Task.WhenAny methods to handle exceptions from multiple tasks.

Here are some examples of handling exceptions in asynchronous code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// An async method that throws an exception
public async Task<int> DivideAsync(int x, int y)
{
    // Simulate some work
    await Task.Delay(1000);

    // Throw an exception if y is zero
    if (y == 0)
    {
        throw new DivideByZeroException();
    }

    // Return the result
    return x / y;
}

// An async method that catches an exception
public async Task HandleExceptionAsync()
{
    try
    {
        // Call an async method that may throw an exception and await its result
        var result = await DivideAsync(10, 0);

        // Print the result
        Console.WriteLine($"Result: {result}");
    }
    catch (DivideByZeroException ex)
    {
        // Handle the exception
        Console.WriteLine($"Error: {ex.Message}");
    }
}

// A synchronous method that inspects a task's status and exception
public void InspectTask()
{
    // Call an async method that may throw an exception without awaiting its result
    var task = DivideAsync(10, 0);

    // Attach a continuation task that runs when the task completes
    task.ContinueWith(t =>
    {
        // Check if the task is faulted
        if (t.IsFaulted)
        {
            // Get the exception from the task
            var ex = t.Exception;

            // Handle the exception
            Console.WriteLine($"Error: {ex.Message}");
        }
        else
        {
            // Get the result from the task
            var result = t.Result;

            // Print the result
            Console.WriteLine($"Result: {result}");
        }
    });
}

Asynchronous I/O Operations

One of the most common scenarios where asynchronous programming is beneficial is performing I/O operations, such as reading and writing files or making web requests. These operations are typically slow and can block the current thread for a long time, causing performance issues and unresponsive applications.

To perform asynchronous I/O operations, you can use various classes and methods in the .NET Framework that support asynchronous programming. For example, you can use:

  • The FileStream, StreamReader, and StreamWriter classes for reading and writing files asynchronously.
  • The HttpClient, HttpResponseMessage, and HttpContent classes for making web requests and reading web responses asynchronously.
  • The Task.Run method for running CPU-bound tasks asynchronously on a thread pool.

By using these classes and methods, you can improve the performance and scalability of your application. For example, you can avoid blocking the main thread while waiting for the I/O operation to complete, and free up resources for other tasks.

Here are some code examples for performing asynchronous I/O operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

// An async method that reads a file asynchronously and returns its content as a string
public async Task<string> ReadFileAsync(string path)
{
    // Create a FileStream object
    using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);

    // Create a StreamReader object
    using var reader = new StreamReader(stream);

    // Await the content of the file
    var content = await reader.ReadToEndAsync();

    // Return the content as a string
    return content;
}

// An async method that writes a string to a file asynchronously
public async Task WriteFileAsync(string path, string content)
{
    // Create a FileStream object
    using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);

    // Create a StreamWriter object
    using var writer = new StreamWriter(stream);

    // Await writing the content to the file
    await writer.WriteAsync(content);
}

// An async method that makes a web request asynchronously and returns the response status code as an int
public async Task<int> MakeRequestAsync(string url)
{
    // Create an HttpClient object
    using var client = new HttpClient();

    // Await the response from the web server
    var response = await client.GetAsync(url);

    // Return the status code as an int
    return (int)response.StatusCode;
}

// An async method that runs a CPU-bound task asynchronously and returns the result as an int
public async Task<int> CalculateAsync(int x, int y)
{
    // Run the task on a thread pool using Task.Run
    var result = await Task.Run(() =>
    {
        // Simulate some work
        System.Threading.Thread.Sleep(1000);

        // Return the result of some calculation
        return x * y;
    });

    // Return the result as an int
    return result;
}

Asynchronous Patterns and Anti-Patterns

Asynchronous programming can be very powerful, but it can also be very complex and error-prone. Therefore, it is important to follow some best practices and avoid some common pitfalls when writing asynchronous code.

Some of the common asynchronous programming patterns are:

  • Fire-and-forget: This pattern is used when you want to start an asynchronous task without waiting for its completion or result. You can use this pattern for tasks that are not critical or do not affect the flow of your application. For example, you can use this pattern to log some information or update some UI element.
  • Asynchronous initialization: This pattern is used when you want to initialize some resource or component asynchronously before using it. You can use this pattern for resources or components that are expensive or time-consuming to create. For example, you can use this pattern to load some data or configuration from a file or a web service.
  • Async/await all the way: This pattern is used when you want to write asynchronous code that is consistent and easy to read. You can use this pattern by using async and await keywords throughout your code, from top to bottom. This way, you can avoid mixing synchronous and asynchronous code, which can cause deadlocks or performance issues.

Some of the common asynchronous programming anti-patterns are:

  • Async over sync: This anti-pattern is used when you want to wrap a synchronous method in an async method without actually making it asynchronous. You can avoid this anti-pattern by using existing asynchronous methods or creating your own asynchronous methods that use Task.Run or other mechanisms.
  • Sync over async: This anti-pattern is used when you want to block an asynchronous method by calling its Result or Wait properties. You can avoid this anti-pattern by using await instead of blocking the current thread. This way, you can prevent deadlocks or performance issues.
  • Async void: This anti-pattern is used when you want to create an async method that returns void. You can avoid this anti-pattern by returning a Task instead of void. This way, you can handle exceptions and monitor the status of the task.

Here are some examples of applying these patterns and avoiding these anti-patterns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

// A fire-and-forget pattern example
public async Task LogMessageAsync(string message)
{
    // Write the message to a file asynchronously without awaiting it (fire-and-forget)
    WriteFileAsync("log.txt", message);
}

// An asynchronous initialization pattern example
public class DataService
{
    // A private field to store the data
    private string data;

    // A public property to get the data
    public string Data => data;

    // An async method to initialize the data asynchronously
    public async Task InitializeAsync()
    {
        // Read the data from a file asynchronously and store it in the field
        data = await ReadFileAsync("data.txt");
    }
}

// An async/await all the way pattern example
public async Task ProcessDataAsync()
{
    // Create a DataService object
    var service = new DataService();

    // Initialize the service asynchronously and await it
    await service.InitializeAsync();

    // Get the data from the service
    var data = service.Data;

    // Do something with the data asynchronously
    DoSomethingAsync(data);
}

// An async over sync anti-pattern example
public async Task<int> AddAsync(int x, int y)
{
    // Call a synchronous method and return its result as a Task (async over sync)
    return x + y;
}

// A sync over async anti-pattern example
public int MakeRequest(string url)
{
    // Call an async method and block its result (sync over async)
    return MakeRequestAsync(url).Result;
}

// An async void anti-pattern example
public async void DoSomethingAsync(string data)
{
    // Do something asynchronously with data and return void (async void)
    await Task.Delay(1000);
}

Testing Asynchronous Code

Testing asynchronous code can be challenging, as you need to deal with concurrency, timing, and exceptions. However, there are some strategies and tools that can help you write effective unit tests for asynchronous code.

Some of the strategies for testing asynchronous code are:

  • Use async and await keywords in your test methods to make them asynchronous.
  • Use Assert methods that support asynchronous operations, such as Assert.ThrowsAsync or Assert.CompletedTask.
  • Use Task.Delay or other mechanisms to simulate delays or timeouts in your asynchronous code.
  • Use mocking frameworks or libraries to mock or stub out external dependencies or services in your asynchronous code.

Some of the testing frameworks and tools that support asynchronous testing in C# are:

  • NUnit: A popular unit testing framework that supports asynchronous testing with async and await keywords, as well as Timeout and ThrowsAsync attributes.
  • xUnit: Another popular unit testing framework that supports asynchronous testing with async and await keywords, as well as Fact and Theory attributes.
  • Moq: A mocking framework that allows you to create mock objects and set up expectations for your asynchronous code.
  • FluentAssertions: A library that provides fluent syntax and extensions for asserting various aspects of your asynchronous code.

Here are some examples of writing unit tests for asynchronous methods using NUnit and Moq:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System.Net.Http;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;

[TestFixture]
public class WebServiceTests
{
    // A mock HttpClient object
    private Mock<HttpClient> mockClient;

    // A WebService object under test
    private WebService webService;

    [SetUp]
    public void SetUp()
    {
        // Create a mock HttpClient object
        mockClient = new Mock<HttpClient>();

        // Create a WebService object with the mock HttpClient object
        webService = new WebService(mockClient.Object);
    }

    [Test]
    public async Task GetContentAsync_ShouldReturnContent_WhenUrlIsValid()
    {
        // Arrange
        var url = "https://example.com";
        var content = "Hello, world!";

        // Set up the mock HttpClient object to return a fake response with the content
        mockClient.Setup(c => c.GetAsync(url))
            .ReturnsAsync(new HttpResponseMessage()
            {
                Content = new StringContent(content)
            });

        // Act
        var result = await webService.GetContentAsync(url);

        // Assert
        Assert.AreEqual(content, result);
    }

    [Test]
    public void GetContentAsync_ShouldThrowException_WhenUrlIsInvalid()
    {
        // Arrange
        var url = "https://invalid.com";

        // Set up the mock HttpClient object to throw an exception when called with the url
        mockClient.Setup(c => c.GetAsync(url))
            .ThrowsAsync(new HttpRequestException());

        // Act and assert
        Assert.ThrowsAsync<HttpRequestException>(() => webService.GetContentAsync(url));
    }
}

Real-World Use Cases

Asynchronous programming is indispensable for many real-world scenarios where you need to write responsive, scalable, and performant applications. Here are some examples of such scenarios:

  • Responsive UIs: Asynchronous programming can help you create user interfaces that are responsive and interactive, without freezing or blocking the UI thread. For example, you can use asynchronous programming to load data from a file or a web service, perform some calculations or validations, or update some UI elements asynchronously.

  • Web API calls: Asynchronous programming can help you make web API calls that are fast and efficient, without blocking the current thread or wasting resources. For example, you can use asynchronous programming to make multiple web requests concurrently, handle errors and timeouts gracefully, or process the responses asynchronously.

  • Database access: Asynchronous programming can help you access databases that are remote or slow, without affecting the performance or scalability of your application. For example, you can use asynchronous programming to execute queries or commands, read or write data, or perform transactions asynchronously.

Conclusion

In this blog post, I have explained how to use the async and await keywords in C# to write asynchronous code that is responsive, scalable, and performant. I have also explained how to create and use async methods, handle multiple asynchronous tasks, handle exceptions in asynchronous code, perform asynchronous I/O operations, apply asynchronous patterns and avoid anti-patterns, and test asynchronous code. You have also seen some real-world scenarios where asynchronous programming is indispensable.

Asynchronous programming is a powerful technique that can improve your C# skills and help you create better applications. I encourage you to explore and practice asynchronous programming to take your C# development to the next level.

Keep Coding…

This post is licensed under CC BY 4.0 by the author.