Asynchronous Programming with async and await in C#


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.
,