Get in touch
Thank you
We will get back to you as soon as possible
.pdf, .docx, .odt, .rtf, .txt, .pptx (max size 5 MB)

4.6.2014

7 min read

5 Things you should know about async/await in C#

Asynchronous programming is becoming more and more popular, either in .Net or some other language. With asynchronous programming, your logic can be divided into available tasks, which can perform some long-running operations such as downloading a resource from the URL, reading a large file, performing a complex calculation or doing an API call without blocking the execution of your application on UI or service. To make you suffer less, .Net framework provides you simple and easy to use keywords, which are the async and await modifiers to transform your code from synchronous to asynchronous. Even though async/await mechanism allows you to do complicated things easier, it is still quite a difficult subject by itself. The article sheds some light on some features and possible pitfalls that you may encounter writing async methods. 1. Return value Async method signature can have three types of the return value: void, Task, and Task. However, the way you return the value in asynchronous methods is not applicable here. Async methods implicitly create the instance of Task for you, which is used as a return value.

public async void ReturnVoidMethodAsync()
{
    await AsyncMethod();
    Console.WriteLine("Finished async method returning void");
}
public async Task ReturnTaskMethodAsync()
{
    await AsyncMethod();
    Console.WriteLine("Finished async method returning Task");
}
public async Task<string> ReturnTaskWithResultMethodAsync()
{
    string text = await AsyncMethod();
    Console.WriteLine("Finished async method returning Task with string result");
    return text;
}

You might have noticed that we never return Task directly. Instead, we return nothing (in case of async void and Task method) or an instance of type T (if async Task method). This simplification improves readability by making the asynchronous method look synchronous. In the case of void return type, the instance of Task is still implicitly created, even though it is not returned to the caller. Generally, it is a bad practice to have void async methods, unless they are used as asynchronous event handlers, where returning a value does not make much sense. Also if you want to implement the “fire and forget” strategy and do not care about it when it is finished. The last option is when you have to implement an interface that does not support Tasks. But in this case, you would rather ask the author of the interface to extend it. 2. Exception handling Any thrown exception within a Task is always wrapped in AggregateException object. Therefore, working with Tasks, we usually deal with aggregate exceptions, which sometimes may be inconvenient. Async/await simplifies exception handling to make code look more synchronous. When you await Task that was terminated by the exception, await mechanism implicitly unwraps propagated AggregateException, leaving you with the underlying exception. Dealing with the unwrapped exceptions is easier and better in terms of code readability.

async void ExceptionUnwrappingTest()
{
    try
            {
        await AsyncMethod();
    }
           catch (TimeoutException ex)
           {
        Debug.WriteLine(ex.ToString());
    }
}
Task AsyncMethod()
{
    return Task.Run(() = >
            {
        throw new TimeoutException();
    });
}

Sometimes AggregateException contains more than one exception. In this case, the first exception is taken from the inner exception list during unwrapping. If you want to handle all inner exceptions, you should do it directly through task object. This is the reason why the code below works. If we change TimeoutException with ArgumentException in the catch statement, the thrown exception will be unhandled.

public static async void ExceptionUnwrappingTest()
{
    Task taskToAwait = Task.WhenAll(FirstAsyncMethod(), SecondAsyncMethod());
    try
            {
        await taskToAwait;
    }
            catch (TimeoutException ex)
            {
        foreach(var exception in taskToAwait.Exception.InnerExceptions)
                       {
            Debug.WriteLine(exception.ToString());
        }
    }
}
private static Task FirstAsyncMethod()
{
    return Task.Run(() = > {
        throw new TimeoutException();
    });
}
private static Task SecondAsyncMethod()
{
    return Task.Run(() = > {
        throw new ArgumentException();
    });
}

When you throw an exception inside async Task or async Task method, it is attached to the task object. However, when you throw it inside the async void method, it cannot be attached to the task, as there is no task. Instead, the exception will be raised on the SynchronizationContext that was active when async void method started.

public void SomeMethod()
{
    try
            {
        MethodAsync();
    }
            catch (Exception exception)
            {
        // The Exception is never caught here
        Debug.WriteLine(exception.ToString());
    }
}
public async void MethodAsync()
{
    throw new Exception();
}

3. Async method inside Async/Await allows us to write asynchronous code in a synchronous manner, therefore it is easy to labor under the delusion that invocation cost in terms of performance is also similar.

public async Task<byte[]> SomeMethodAsync()
{
    var webClient = new WebClient();
    var data = await webClient.DownloadDataTaskAsync("http://www.google.com/");
    return data;
}

Unfortunately, it is far from the truth. Async/await is syntactic sugar, not available at IL/CLR level. For that reason compiler will generate state machine based on the method above, full of much boilerplate code. The more awaits you have inside the method body, the bigger resulting state machine becomes, transforming what seems as several statements into a long chain of conditions and commands. It drastically increases invocation cost of such a method in comparison with synchronous method. Another reason that may decrease performance is object allocations. Async/await causes implicit task object creation, required to be returned to the caller. This leads to one of the largest performance costs in an asynchronous method, which is calling a Garbage Collector. If such asynchronous method is frequently invoked it will result in a large number of created task objects needed to be garbage collected. You can fix it by caching tasks and invoking the async method only when the cache does not have an appropriate task.

private ConcurrentDictionary<string, Task<string>> _contentTasks =
    new ConcurrentDictionary<string, Task<string>>();
public Task<string> GetContentAsync(string url)
{
    if (_contentTasks.TryGetValue(url, out var contentTask))
            {
        contentTask = LoadContentAsync(url);
        contentTask.ContinueWith(task => _contentTasks.TryAdd(url, task),
            TaskContinuationOptions.OnlyOnRanToCompletion |
            TaskContinuationOptions.ExecuteSynchronously);
                       return contentTask;
    }
    return Task.FromResult(null);
}
private Task<string> LoadContentAsync(string url)
{
    return new WebClient().DownloadStringTaskAsync(url);
}

4. Context capturing Async methods implicitly captures current synchronization context. Then, the context is used to execute the async method code after awaiting is finished. It means that the code before and after “await” statement will be executed on a thread owning captured context. When does it prove useful? For example, in UI and ASP.NET applications, where you can modify or access objects only on the thread, that has created them. Without synchronization context, async method code will be executed using thread pool context on any currently available thread. (Fortunately, AspNetSynchronizationContext was removed in ASP.NET Core. In addition, the async/await mechanism is highly optimized for the contextless scenario. There is simply less work to do for asynchronous requests. For more information you can read this comprehensive article by Stephen Cleary). Here is an example of some WPF element event handler:

public async void OnSomeEvent (TextBox textBox)
{
    // enter method on thread #1 (UI thread).
    textBox.Text = string.Empty;
    // launch asynchronous text retrieval on thread #2 (from thread pool) and return.
    var text = await GetTextAsync();
    // after awaiting is finished continue executing on thread #1.
    textBox.Text = text;
}

Here, we start running the method on a UI thread. After encountering await statement we return to the caller. The code following await statement would be executed after awaiting is finished. Thanks to the captured context it will be done on UI thread. If it were not for capturing context we would end up updating textbox object from a non-UI thread, that would lead us to an exception. However, there are many cases when we do not need to switch contexts and execute code on a specific thread. Not only may it be unnecessary, but it also causes a performance loss. In this case, we can setup how we want to await an asynchronous operation using ConfigureAwait method.

public async Task<IEnumerable<double>> GetRaisedValuesAsync()
{
    // enter method on thread #1
    Debug.WriteLine("Start computing values");
    // launch computing values on thread #2 and return to the caller
    var valueList = await ComputeValuesAsync().ConfigureAwait(false);
    // continue method execution on thread #3
    var raisedValueList = valueList.Select(value = > Math.Pow(value, 2));
    return raisedValueList;
}

The code above uses ConfigureAwait(false) to prevent context capturing, therefore after awaiting remaining code is executed on any available thread from the thread pool. 5. Awaiting different types You might have noticed, in the previous example, we await an object that is not of Task type. It is because an object can be awaited if it implements the awaitable/awaiter pattern. This pattern expects from type: 1. Have a GetAwaiter() method, which returns the awaiter object 2. Returning awaiter must implement INotifyCompletion or ICriticalNotifyCompletion interface, have IsCompleted Boolean property and GetResult() method, which returns void, or a result. For example, we can create asynchronous lazy initializer class:

class LazyAsyncInitializor<T> : Lazy<Task<T>>
{
    public LazyAsyncInitializor(Func<T> valueFactory)
                  : base(() = > Task.Run(valueFactory)) {}
    public TaskAwaiter<T> GetAwaiter()
            {
        return Value.GetAwaiter();
    }
}
 
Then we await instance of LazyAsyncInitializor, asynchronously retrieving data.
 
public class RemoteDataHandler
{
    private LazyAsyncInitializor<IEnumerable<byte>> _dataAsyncInitializor;
    public RemoteDataHandler()
            {
        _dataAsyncInitializor =
            new LazyAsyncInitializor<IEnumerable<byte>>
        (LoadRemoteDataAsync);
    }
    public async Task ProcessData()
            {
        IEnumerable<byte> data = await _dataAsyncInitializor;
        // process retrieved data...
    }
}

As you can see, there are no dedicated interfaces or abstractions for awaitable/awaiter pattern in .NET. This allows us to implement GetAwaiter() method as an extension for existent types, making them awaitable.

public static class ActionExtensions
{
    public static TaskAwaiter GetAwaiter(this Action action)
            {
        Task task = Task.Run(action);
        return task.GetAwaiter();
    }
}

Now we can await action delegate like we do this with tasks.

public async void Test()
{
    Action sleepAction = () = > Thread.Sleep(3000);
    await sleepAction;
}

These are not all possible traps you can face working with Tasks. But keeping in mind all complexities around this fancy async/await mode, you will be ready to solve advanced issues such as deadlocks or race conditions.

Thank you
We will get back to you as soon as possible

Looking for a tech partner to help you scale your project?

Let’s schedule a quick call to explore how we can support your business objectives.

Edward Robe

Let’s schedule a quick call to explore how we can support your business objectives.

Edward Robe

Senior Client Manager

0 Comments
name *
email *

Featured Case Studies