Asynchronous Programming Overview
The asynchronous programming paradigm is a programming technique which defines a piece of work as being run independently from the main application thread. It will notify the main thread once the work is done when the operation completes or fails. By contrast, this approach lets applications remain responsive to user interactions or other events while doing costly stuff like file I/O, network communication and database lookup.
The Usefulness of Modern Applications
In today’s software, one application needs to do many things at once; pull data from the internet, read a file, interact with a database. Asynchronous programming is crucial for:
- Responsiveness: Ensuring that the user interfaces remain active and responsive while a long running operation takes place.
- Scalability: An effective use of resources by server applications so as to handle more requests.
- Performance: Increased overall application throughput as well as performance.
- Real-World Scenarios
- Web Applications: Not blocking of the thread with many client requests.
- Desktop Applications: Making background tasks(like data loading) without freezing the UI.
- Mobile Apps: The important issues of fetching data over a network and giving the user a smooth experience.
Synchronous challenges with Code
Synchronous code takes care of executing tasks, one at a time, blocking the current thread until the operation completes. This can lead to:
- Unresponsive UIs: In a GUI application, long operations can freeze the interface.
- Inefficient Resource Use: Even when threads are waiting for I/O operation to finish they remain idle themselves.
- Poor Scalability: Less requests can be handled by servers as threads are blocked.
Asynchronous Programming in C# – Evolution
- Callbacks: Delegates or event handlers would have become the early solution, finishing up with callback’s hell.
- Asynchronous Programming Model (APM): It is based on Begin methods and End methods.
- Event-based Asynchronous Pattern (EAP): Signals completion through usage of events.
- Task-based Asynchronous Pattern (TAP): This is introduced in .NET Framework 4, using Task and Task<TResult>.
- async and await: Made available in C# 5.0 to ease developers in writing the asynchronous code in a linear format.
Understanding Asynchronous Programming
Synchronous vs. Asynchronous Executes
Synchronous Execution: One task after the other is performed. The next only starts when the previous finishes.
public void PerformTasks()
{
Task1();
Task2();
Task3();
}
Asynchronous Execution: Other code can run at the same time as the tasks do, and tasks can begin and keep running in the background.
public async Task PerformTasksAsync()
{
Task task1 = Task1Async();
Task task2 = Task2Async();
Task task3 = Task3Async();
await Task.WhenAll(task1, task2, task3);
}
Concurrency vs. Parallelism
Concurrency: Multiple tasks that are schedulable for partial overlaps between different task periods.
Parallelism: The act of doing several things at once, usually on several processors or cores.
And finally, the realm of asynchronous programming is mostly about concurrency, in which you can have an application that runs several independent programs that do not have to be executed sequentially, but each one is interleaved with the other(s).
Threads and the Thread Pool
Threads: The basic unit of execution. It means that each thread can be responsible for execution of code independently.
Thread Pool: A collection of worker threads maintained by the .NET runtime to be used for short lived operations.
In C# asynchronous programming often uses the thread pool to execute threads in a thread pool without the need for manual intervention.
Task Based Asynchronous Pattern (TAP)
Introduction to TAP
The Task and Task<TResult> types standardize asynchronous programming using the Task based Asynchronous Pattern (TAP). It gives us a uniform way to represent asynchronous operations and also allows for chaining composition and so on.
Task and Task<TResult>
Task: It’s an asynchronous operation without a return value.
Task<TResult>: An asynchronous operation that returns type TResult.
Continuation Tasks and Chaining
Prior to async
and await
, developers used ContinueWith
to chain tasks:
Task.Run(() => DoWork())
.ContinueWith(t => ProcessResult(t.Result), TaskScheduler.FromCurrentSynchronizationContext());
It also can become complex and difficult to read very quickly.
Introducing async
and await
The async
Keyword
- Marks a method as asynchronous.
- Allows the use of the
await
keyword within the method. - Transforms the method into a state machine that manages the asynchronous operations.
The await
Operator
- Pauses the execution of an async method until the awaited
Task
completes. - The method resumes after the
Task
completes, potentially on a different thread. - Simplifies asynchronous code to appear linear.
public async Task<string> FetchContentAsync(string url)
{
using HttpClient client = new HttpClient();
string content = await client.GetStringAsync(url);
return content;
}
Defining Asynchronous Methods
Return Types:
- Task: For methods that don’t return a value.
- Task<TResult>: Methods that return a value of type TResult.
- void: Only for event handlers.
The Execution Context
- Synchronization Context: This context is where the asynchronous operation was started.
- It determines the location of the where we want the continuation after await to run.
- In UI applications, we want to be able to update UI elements on the main thread.
Writing Asynchronous Code
Convert Synchronous methods to asynchronous
Find how to detect I/O bound operations that would be helpful with asynchrony.
Synchronous Method:
public string ReadFile(string filePath)
{
using StreamReader reader = new StreamReader(filePath);
return reader.ReadToEnd();
}
Asynchronous Method:
public async Task<string> ReadFileAsync(string filePath)
{
using StreamReader reader = new StreamReader(filePath);
return await reader.ReadToEndAsync();
}
Asynchronous File I/O Operations
Stream class and its descendants provides asynchronous methods for file operations.
public async Task WriteTextAsync(string filePath, string content)
{
using StreamWriter writer = new StreamWriter(filePath);
await writer.WriteAsync(content);
}
Asynchronous Network Operations
Using HttpClient
for network calls:
public async Task<string> DownloadDataAsync(string url)
{
using HttpClient client = new HttpClient();
string data = await client.GetStringAsync(url);
return data;
}
Async in Database Operations
Database interactions come with async methods included in Entity Framework Core.
public async Task<List<Product>> GetProductsAsync()
{
using var context = new MyDbContext();
return await context.Products.ToListAsync();
}
Handling Exceptions in Async Methods
Async Methods with Exception propagation
- Exceptions thrown in async method are captured and re thrown when the task is awaited.
- Exceptions that are not handled will fault the task.
public async Task<string> GetDataAsync(string url)
{
using HttpClient client = new HttpClient();
string data = await client.GetStringAsync(url);
return data;
}
// Elsewhere
try
{
string result = await GetDataAsync("http://invalid-url");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
The AggregateException
Class
With multiple tasks to wait on, we may wrap an AggregateException around the they exceptions.
Task[] tasks = { Task1Async(), Task2Async() };
try
{
await Task.WhenAll(tasks);
}
catch (Exception)
{
foreach (var t in tasks)
{
if (t.Exception != null)
{
foreach (var ex in t.Exception.InnerExceptions)
{
Console.WriteLine(ex.Message);
}
}
}
}
Best Practices for Asynchronous Programming
Avoiding Blocking Calls
- Do not use
.Wait()
or.Result
on tasks; this blocks the calling thread. - Example of what not to do:
public void DoWork()
{
var result = SomeAsyncMethod().Result; // Blocks the thread
}
Async All the Way
- Since we are marking any method async once we start invoking it, all methods that are calling it should be async.
- It does not block and at the same time does not loose responsiveness.
Returning Task
Instead of void
- The purpose of async methods is to be able to await them, thus they should return Task or Task<TResult>.
- The only things event handlers can return are void.
ConfiguringAwait Appropriately
If you implement the library, avoid capturing synchronization context by using ConfigureAwait(false).
await SomeAsyncMethod().ConfigureAwait(false);
Cancellation Tokens
We should use CancellationToken to allow for cooperative cancellation.
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
// Pass the token to async methods that support cancellation
await SomeOperationAsync(cancellationToken);
}
Progress Reporting
To report progress from async methods you use IProgress.
public async Task DownloadFileAsync(string url, IProgress<int> progress)
{
// Report progress as needed
progress.Report(50); // 50% complete
}
How to Avoid (Prevent) Common Pitfalls
Synchronization Context Deadlocks
Deadlocks can occur in UI applications since blocking the main thread.
// Potential deadlock
public void DoWork()
{
var result = SomeAsyncMethod().Result;
}
Solution: It means to use the future await rather than blocking calls.
Not Awaiting Tasks
Failure to wait for a task will cause unhandled exceptions or unfinished work.
public async Task DoWorkAsync()
{
SomeAsyncMethod(); // Should be awaited
await AnotherAsyncMethod();
}
Mixing Async and Sync Code Improperly was another pitfall
- Each benefit of asynchrony can be negated if you mix synchronous and asynchronous code.
- Make sure that asynchronous methods are awaited and that synchronous methods don’t block.
Unobserved Task Exceptions
- A task that isn’t awaited may expose exceptions.
- Or, use task exception handing mechanisms or await all tasks and not use async/await.
Asynchronous Programming: Advanced Topics
Asynchronous vs Parallel Programming
- Asynchronous Programming: Tasks that may not run at the same time, but can grant control.
- Parallel Programming: Multiple tasks at the same times, usually with multiple cores.
Example combining both:
public async Task ProcessDataInParallelAsync()
{
var data = await GetDataAsync();
await Parallel.ForEachAsync(data, async (item, _) =>
{
await ProcessItemAsync(item);
});
}
Asynchronous Streams
For making the stream of data work asynchronously, we use IAsyncEnumerable and await foreach.
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100);
yield return i;
}
}
public async Task ConsumeNumbersAsync()
{
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine(number);
}
}
Concurrency and Synchronization
Use synchronization primitives like SemaphoreSlim
to control concurrency in async code.
private SemaphoreSlim _semaphore = new SemaphoreSlim(1);
public async Task AccessResourceAsync()
{
await _semaphore.WaitAsync();
try
{
// Access shared resource
}
finally
{
_semaphore.Release();
}
}
Working with Task.WhenAll
and Task.WhenAny
Task.WhenAll: It awaits multiple tasks to complete.
var tasks = new List<Task> { Task1Async(), Task2Async() };
await Task.WhenAll(tasks);
Task.WhenAny: When any one of the tasks completes:
var tasks = new List<Task> { Task1Async(), Task2Async() };
var completedTask = await Task.WhenAny(tasks);
Custom Awaitables
Make types that you can await to be created by implementing the GetAwaiter method.
public class MyAwaitable
{
public MyAwaiter GetAwaiter() => new MyAwaiter();
}
public class MyAwaiter : INotifyCompletion
{
public bool IsCompleted => true;
public void OnCompleted(Action continuation) { }
public void GetResult() { }
}
// Usage
await new MyAwaitable();
Asynchronous Programming in Different Application Types
Console Applications
And from C# 7.1 on, this is also an entry point that can be asynchronous.
public static async Task Main(string[] args)
{
await DoWorkAsync();
}
Continuations run on thread pool threads by default and have no synchronization context.
WPF, Windows Forms GUI Applications
- With synchronization context you are sure UI is updated (or running) on the main thread.
- Don’t put the UI thread in deadlock.
private async void Button_Click(object sender, EventArgs e)
{
await DoWorkAsync();
// Safe to update UI elements here
}
ASP.NET and ASP.NET Core Applications
- In ASP.NET, code would run on the request thread with synchronization context.
- By default, there is no synchronization context in ASP.NET Core.
- Improving Scalability:
- Async methods implies they free a thread to handle some other request.
- Don’t block the calls that may starve your thread.
Performance Considerations
Overhead of Async Methods
- Async methods are state machines with small overhead.
- In the case of methods that complete synchronously, when they return Task.CompletedTask or if they are synchronous you are good to go.
Using ValueTask
When a frequently called async method completes synchronously, ValueTask reduces allocations.
public ValueTask<int> GetValueAsync()
{
if (cached)
return new ValueTask<int>(cachedValue);
else
return new ValueTask<int>(ComputeValueAsync());
}
Trade-offs:
- Less and more difficult to use correctly.
- It should not be used indiscriminately.
Optimizing Async Code
- Avoid Unnecessary Async: If a method doesn’t do asynchronous operations, it doesn’t need to be async.
- Reuse Tasks: Where it’s appropriate, cache and reuse tasks.
Benchmarking and Profiling
- Tools to use are: BenchmarkDotNet, Visual Studio Profiler, or dotTrace.
- Ensuring made changes improve performance before and after.
Testing Asynchronous Code
Unit Testing Async Methods
And test methods should also be marked as async.
[TestMethod]
public async Task TestDoWorkAsync()
{
await DoWorkAsync();
// Assert results
}
Working with Synchronization context in Tests
- Some of the testing frameworks indeed remove this synchronization context during tests.
- In tests, make sure async code doesn’t need a synchronization context.
Mocking Async Methods
Mocking frameworks for asynchronous methods, if and when needed.
var mock = new Mock<IMyService>();
mock.Setup(s => s.GetDataAsync()).ReturnsAsync("Mocked Data");
Conclusion
Recap of Key Concepts
- Asynchronous programming helps make your app more responsive, and more scalable.
- async and await make writing asynchronous code less of a hassle.
- Well used, asynchronous programming patterns result in more efficient applications.
Asynchronous Programming Mastering Benefits.
- Improved User Experience: Response applications remain responsive.
- Better Resource Utilization: This way, we can efficiently use threads, system resources.
- Scalability: Ability to perform more tasks at the same time.