c-sharp concurrency async await

we have discussed concurrency categories in C# in last chapter. This chapter I will talk about how to use basic async and await for asynchronous operations. I will introduce different scenarios to use async and await to resolve them.

Pausing for a Period of Time

This scenario problem is pretty common when doing unit test or implementing retry delays. The following code shows how to use the static method Delay to resolve this issue.

				
					async Task<T> DelayResult<T>(T result, TimeSpan delay)
{
    await Task.Delay(delay);
    return result;
}
				
			

This case returns a task with asynchronous success result.

When increasing the delays between retries, the exponential backoff is a better strategy. It can limit retries when doing web services. Exponential backoff code:

				
					async Task<string> DownloadStringWithRetries(HttpClient client, string uri)
{
    // Retry after 1 second, then after 2 seconds, then 4.
    TimeSpan nextDelay = TimeSpan.FromSeconds(1);
    for (int i = 0; i != 3; ++i)
    {
        try
        {
            return await client.GetStringAsync(uri);
        }
        catch
        {
        }
        await Task.Delay(nextDelay);
        nextDelay = nextDelay + nextDelay;
    }
    // Try one last time, allowing the error to propagate.
    return await client.GetStringAsync(uri);
}
				
			

Tip: some third-party libraries in NuGet are more thorough as a solution to resolve this, such as Polly.

To avoid infinite delay retries, you can use CancellationTokenSource to wrap the code. Using a cancellation token to cancel a task after a specific time. And then using Task.WhenAny to implement the “soft timeout”. Please see the code:

				
					async Task<string> DownloadStringWithTimeout(HttpClient client, string uri)
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
    Task<string> downloadTask = client.GetStringAsync(uri);
    Task timeoutTask = Task.Delay(Timeout.InfiniteTimeSpan, cts.Token);
    Task completedTask = await Task.WhenAny(downloadTask, timeoutTask);
    if (completedTask == timeoutTask)
        return null;
    return await downloadTask;
}
				
			

The limitation of above method is that a task won’t be cancelled if its operation times out. The preferred way to cancel a task is to use a cancellation token as a timeout and pass it directly to the operation (GetStringAsync in the previous example). Therefore, Task.Delay is a good choice when handling retry logic or implementing unit testing asynchronous code.

Returning Completed Tasks

The scenario here is that implementing code synchronously in an asynchronous class. This is more suitable for asynchronous unit testing. And the Task.FromResult is the solution for this to return a created Task<T>. Please see the code:

				
					interface IMyAsyncInterface
{
    Task<int> GetValueAsync();
}
class MySynchronousImplementation : IMyAsyncInterface
{
    public Task<int> GetValueAsync()
    {
        return Task.FromResult(13);
    }
}
				
			

If you don’t have to return a value for a method, you can use Task.CompletedTask. Please see the code:

				
					interface IMyAsyncInterface
{
    Task DoSomethingAsync();
}
class MySynchronousImplementation : IMyAsyncInterface
{
    public Task DoSomethingAsync()
    {
    return Task.CompletedTask;
    }
}

// This is returning an exception if you need a non-successful result
Task<T> NotImplementedAsync<T>()
{
    return Task.FromException<T>(new NotImplementedException());
}

// Similar situation: using Task.FromCanceled to return a cancelled task with a given CancellationToken
Task<int> GetValueAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
        return Task.FromCanceled<int>(cancellationToken);
    return Task.FromResult(13);
}
				
			

Avoiding any type of blocking is very critical when implementing synchronous code in an asynchronous method or interface. Blocking an asynchronous method may cause a deadlock, because it prevents calling threads from starting other tasks.

Reporting Progress

C# provides IProgress<T> and Progress<T> types to handle this situation. Checkout the code:

				
					async Task MyMethodAsync(IProgress<double> progress = null)
{
    bool done = false;
    double percentComplete = 0;
    while (!done)
    {
        ...
        progress?.Report(percentComplete);
    }
}

//Calling code can use it as such:
async Task CallMyMethodAsync()
{
    var progress = new Progress<double>();
    progress.ProgressChanged += (sender, args) =>
    {
        ...
    };
    await MyMethodAsync(progress);
}
				
			

It’s better to define the T type as an immutable type (what is an immutable type, you can check: here), ex IProgress<double>, to avoid that the MyMethodAsync method may still continue executing before the progress is reported. Also, it’s good to support cancellation if a method supports progress reporting.

Waiting for a Set of Tasks to Complete

This is a common scenario. You want to wait for your all tasks to complete. And the Task.WhenAll method is a very useful method to help you handle it.

				
					Task task1 = Task.Delay(TimeSpan.FromSeconds(1));
Task task2 = Task.Delay(TimeSpan.FromSeconds(2));
Task task3 = Task.Delay(TimeSpan.FromSeconds(1));
await Task.WhenAll(task1, task2, task3);
				
			

If all tasks have the same result type, you can return an array where all the task results are.

				
					Task<int> task1 = Task.FromResult(3);
Task<int> task2 = Task.FromResult(5);
Task<int> task3 = Task.FromResult(7);
int[] results = await Task.WhenAll(task1, task2, task3);
// "results" contains { 3, 5, 7 }
				
			

Now let’s talk about the exceptions. The Task.WhenAll can handle all exceptions from those tasks that throw an exception. But await only allow to throw one of those exceptions. Therefore, you need to capture each exception on the Task returned.

				
					async Task ThrowNotImplementedExceptionAsync()
{
    throw new NotImplementedException();
}
async Task ThrowInvalidOperationExceptionAsync()
{
    throw new InvalidOperationException();
}
async Task ObserveOneExceptionAsync()
{
    var task1 = ThrowNotImplementedExceptionAsync();
    var task2 = ThrowInvalidOperationExceptionAsync();
    try
    {
        await Task.WhenAll(task1, task2);
    }
    catch (Exception ex)
    {
        // "ex" is either NotImplementedException or InvalidOperationException.
        ...
    }
}
async Task ObserveAllExceptionsAsync()
{
    var task1 = ThrowNotImplementedExceptionAsync();
    var task2 = ThrowInvalidOperationExceptionAsync();
    Task allTasks = Task.WhenAll(task1, task2);
    try
    {
        await allTasks;
    }
    catch
    {
        AggregateException allExceptions = allTasks.Exception;
        ...
    }
}
				
			

Waiting for Any Task to Complete

One scenario you may encounter in real world is that the code only needs to respond as long as one of those tasks is completed. The Task.WhenAny is our solution for this:

				
					async Task<int> FirstRespondingUrlAsync(HttpClient client,
string urlA, string urlB)
{
    // Start both downloads concurrently.
    Task<byte[]> downloadTaskA = client.GetByteArrayAsync(urlA);
    Task<byte[]> downloadTaskB = client.GetByteArrayAsync(urlB);
    // Wait for either of the tasks to complete.
    Task<byte[]> completedTask =
    await Task.WhenAny(downloadTaskA, downloadTaskB);
    // Return the length of the data retrieved from that URL.
    byte[] data = await completedTask;
    return data.Length;
}
				
			

One point you should pay attention to is the Task.WhenAny never completes in a faulted or canceled state. Therefore, you should observe any exceptions after any task has completed by using await.

Warning: It is not recommended to use Task.WhenAny to implement timeouts like one of tasks is Task.Delay. And another anti-pattern thing is that do not remove other tasks when one task has completed. It will cause performance issue!

Processing Tasks as They Complete

This section to resolve the scenario that processing each task as soon as it completes without waiting for any of the other tasks. We just need to use await to process results.

Here I’m going to present other approaches we can use to solve this problem.

The easiest one is to use higher-level: async and await.

				
					async Task<int> DelayAndReturnAsync(int value)
{
    await Task.Delay(TimeSpan.FromSeconds(value));
    return value;
}
async Task AwaitAndProcessAsync(Task<int> task)
{
    int result = await task;
    Trace.WriteLine(result);
}
// This method now prints "1", "2", and "3".
async Task ProcessTasksAsync()
{
    // Create a sequence of tasks.
    Task<int> taskA = DelayAndReturnAsync(2);
    Task<int> taskB = DelayAndReturnAsync(3);
    Task<int> taskC = DelayAndReturnAsync(1);
    Task<int>[] tasks = new[] { taskA, taskB, taskC };
    IEnumerable<Task> taskQuery =
    from t in tasks select AwaitAndProcessAsync(t);
    Task[] processingTasks = taskQuery.ToArray();
    // Await all processing to complete
    await Task.WhenAll(processingTasks);
}
				
			

Let’s take a look at another refactored version. This version is the cleanest and most portable way to solve this scenario:

				
					async Task<int> DelayAndReturnAsync(int value)
{
    await Task.Delay(TimeSpan.FromSeconds(value));
    return value;
}
// This method now prints "1", "2", and "3".
async Task ProcessTasksAsync()
{
    // Create a sequence of tasks.
    Task<int> taskA = DelayAndReturnAsync(2);
    Task<int> taskB = DelayAndReturnAsync(3);
    Task<int> taskC = DelayAndReturnAsync(1);
    Task<int>[] tasks = new[] { taskA, taskB, taskC };
    Task[] processingTasks = tasks.Select(async t =>
    {
        var result = await t;
        Trace.WriteLine(result);
    }).ToArray();
    // Await all processing to complete
    await Task.WhenAll(processingTasks);
}
				
			

Avoiding Context for Continuations

Since async method will resume executing within the same context by default, this can cause performance problems like in UI context! To resolve this we can pass false for continueOnCapturedContext parameter:

				
					async Task ResumeOnContextAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    // This method resumes within the same context.
}
async Task ResumeWithoutContextAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    // This method discards its context when it resumes.
}
				
			

Too many UI thread can cause performance issues. The fine number of UI threads is like this: 100 threads/s is OK, however around 1000 threads/s is too many!

TIP: There is no hurt to use await ConfigureAwait and pass false to the continueOnCapturedContext parameter.

Handling Exceptions from async Task Methods

It’s critical to handle exceptions in any design. Developers can design for the success case easily. However, how to handle the failure cases decides if a design is correct.

Generally, a simple try/catch can handle most of cases:

				
					async Task ThrowExceptionAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    throw new InvalidOperationException("Test");
}
async Task TestAsync()
{
    try
    {
        await ThrowExceptionAsync();
    }
    catch (InvalidOperationException)
    {
    }
}
				
			

Exceptions are only addressed when a await Task has returned. Check out the code:

				
					async Task ThrowExceptionAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    throw new InvalidOperationException("Test");
}
async Task TestAsync()
{
    // The exception is thrown by the method and placed on the task.
    Task task = ThrowExceptionAsync();
    try
    {
        // The exception is re-raised here, where the task is awaited.
        await task;
    }
    catch (InvalidOperationException)
    {
        // The exception is correctly caught here.
    }
}
				
			

Handling Exceptions from async void Methods

Just like the title, here we are talking about how to handle async void methods. There is no good way to handle exceptions in an async void method. It is best to avoid propagating exceptions outside of the asynchronous void method. However, if you must use the async void method, consider wrapping all of its code in a try block and handling exceptions directly. 

				
					sealed class MyAsyncCommand : ICommand
{
    async void ICommand.Execute(object parameter)
    {
        await Execute(parameter);
    }
    public async Task Execute(object parameter)
    {
        ... // Asynchronous command implementation goes here.
    }
    ... // Other members (CanExecute, etc.)
}
				
			

Also, if you can avoid to use async void methods, that will be great! Using async Task instead of async void is that Task-returning methods are easier to test!

Creating a ValueTask

Generally, a Task can return Task<T> or ValueTask<T> type. For the ValueTask<T> type, a synchronous method can return it usually, which is very rare for an asynchronous method. This section, we are going to talk about how to implement a method returns ValueTask<T>.

Still, we use async and await to implement a method that returns ValueTask<T>, which is the easiest way to do it.

				
					public async ValueTask<int> MethodAsync()
{
    await Task.Delay(100); // asynchronous work.
    return 13;
}
				
			

Here is the an optimized version after refactoring the code. You can return a ValueTask<T> result in a slow asynchronous method if necessary:

				
					public ValueTask<int> MethodAsync()
{
    if (CanBehaveSynchronously)
        return new ValueTask<int>(13);
    return new ValueTask<int>(SlowMethodAsync());
}
private Task<int> SlowMethodAsync();
				
			

TIP: If you want to use ValueTask or ValueTask<T> as the returned value in an asynchronous method, just use async and await.

Consuming a ValueTask

Last section, we talked about how to implement an asynchronous method that returns ValueTask<T>. Now here we are going to discuss how to use it!

Like other scenarios, the await can be used here as a solution:

				
					ValueTask<int> MethodAsync();

async Task ConsumingMethodAsync()
{
    int value = await MethodAsync();
}
				
			

WARNING: Be aware of that in some cases, a ValueTask or ValueTask<T> can be awaited once!

TIP: The ValueTask<T> can be converted into a Task<T> via invoking AsTask, and you can do more complex things. And a Task<T> can be awaited multiple times:

				
					ValueTask<int> MethodAsync();

async Task ConsumingMethodAsync()
{
    Task<int> task = MethodAsync().AsTask();
    ... // other concurrent work
    int value = await task;
    int anotherValue = await task;
}

// await for multiple times to use them later

ValueTask<int> MethodAsync();

async Task ConsumingMethodAsync()
{
    Task<int> task1 = MethodAsync().AsTask();
    Task<int> task2 = MethodAsync().AsTask();
    int[] results = await Task.WhenAll(task1, task2);
}
				
			

WARNING: Synchronizing results from ValueTask or ValueTask can only be performed once after ValueTask is complete, and cannot wait for the same ValueTask or convert it to a task.

If you want to know more details about the concurrency in C#, here is the chapter 1 of concurrency in C#: https://quietbookspace.com/chapter-1-concurrency-overview-in-c-sharp/

Hits: 122

Leave a Reply

Your email address will not be published. Required fields are marked *