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:
- Add the
async
modifier to the method signature. - Specify a return type of
Task
orTask<T>
, whereT
is the type of the result (if any). - Use the
await
keyword to await one or more asynchronous tasks inside the method body. - Optionally, use the
return
keyword to return a result (if any). - 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 insideasync
methods to catch exceptions that occur within them. - Use
await
to propagate exceptions from awaited tasks to the caller. - Use
Task.IsFaulted
,Task.Exception
, andTask.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
andTask.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
, andStreamWriter
classes for reading and writing files asynchronously. - The
HttpClient
,HttpResponseMessage
, andHttpContent
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
andawait
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 useTask.Run
or other mechanisms. - Sync over async: This anti-pattern is used when you want to block an asynchronous method by calling its
Result
orWait
properties. You can avoid this anti-pattern by usingawait
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 returnsvoid
. You can avoid this anti-pattern by returning aTask
instead ofvoid
. 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
andawait
keywords in your test methods to make them asynchronous. - Use
Assert
methods that support asynchronous operations, such asAssert.ThrowsAsync
orAssert.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
andawait
keywords, as well asTimeout
andThrowsAsync
attributes. - xUnit: Another popular unit testing framework that supports asynchronous testing with
async
andawait
keywords, as well asFact
andTheory
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…