DEV Community

Mark Clearwater
Mark Clearwater

Posted on • Originally published at blog.csmac.nz on

Looking Back on C#: async and await

Looking Back on C#: async and await

With C# 8 on our doorstep, I figure it is a good time to reflect on recent additions to the language that have come before. There are some great improvements you may have missed, some that I really enjoy using, and some I consider have reached canonical usage status that I think are all worth some reflection.

Multithreaded programming has always been a difficult thing to get your head around, and there are many pitfalls easily stumbled into. To help combat this, Microsoft gave us async/await in C#.

Async/Await is a language feature that has been around since Visual Studio 2012 and C# 5 and hides a bunch of the boilerplate state machine code required to safely park a logical thread of execution while it waits for some work to complete or to respond from another thread, IO, or network device. This allows code to be more logically procedural and linear, therefore easier to read and comprehend.

Since it first came out there has been a raft of improvements across different versions of C#.

The basics

The foundation of how it all works rests on the shoulders of a Library, the Task Parallel Library, or TPL. Tasks have been around since 2010 and were part of the .Net 4 Framework. Similar to what Promises provide in javascript, this library allowed a logical chain of execution across waiting for blocking or longrunning execution while releasing the UI thread from being blocked. This was the introduction of TaskFactory and Task, in a fairly similar form to what we have today. This was a huge improvement from the days of callback chaining because it reduced heavy nesting of lambdas into more of a linear pipeline and a clear place for error handling to take place.

When C# 5 introduced async/await as a first-class language feature, it was able to leverage the library and extend on it to give us more readable code. It even handled Exceptions by throwing them in the place where await appeared, making try/catch blocks useful in asynchronous code.

What does async/await look like in C# 5?

// A classic synchronous method
public string MakeAWebRequest(Uri uri)
{
    // WebClient has synchronous methods, but it is recommended to use HttpClient for newer apps
    var client = new WebClient();
    return client.DownloadString(uri);
}

// It's async younger brother
public async Task<string> MakeAWebRequestAsync(Uri uri)
{
    // I use WebClient again for better comparing. Use HttpClient!
    var client = new WebClient();
    return await client.DownloadStringAsync(uri);
}

Enter fullscreen mode Exit fullscreen mode

There are two distinctive features in this comparison. The return type is wrapped in a Task<T>, and there is a keyword async on the method signature with await beside method calls that return Task<T> results. Otherwise, the linear execution flow is largely unchanged.

You can easily use the return type without the keywords. In this case, the code works and operates as normal, passing object references around without any async state. The result object captures the state required for the caller to do the asynchronous work in the future, or respond to its completion. However, if you do use async and await, then you should always return either Task (where usually returns void) or Task<T> (where usually returns T). (In my opinion, there are no reasons left to ever do async without Task - there used to be but not anymore.)

Now we have that out of the way, let's move forward to C# 6 and beyond!

Async and Exceptions

Exception handling was a big part of this feature on day one. You could simply wrap your async calls in a try{}catch{} and it would work as you would expect it to. The task you are awaiting throws an Exception, your catch triggers.

But initially, this did not work inside the catch or finally blocks in any expected way. In fact, it caused a compiler error. In C# 6 await in catch/finally blocks were given proper compiler support to do the right thing.

Using the example from The new language feature docs directly:

Resource res = null;
try
{
    res = await Resource.OpenAsync(…); // You could do this.
    …
} 
catch(ResourceException e)
{
    await Resource.LogAsync(res, e); // Now you can do this …
}
finally
{
    if (res != null) await res.CloseAsync(); // … and this.
}

Enter fullscreen mode Exit fullscreen mode

Async and Console Apps

The language version was C# 7. We had async everywhere, and the NetStandards and Frameworks were full of async API calls and interfaces. It was a contagious thing, and you really had to jump through hoops to try to call a red function from a blue one.

And then there was main. That pesky little entry point into your application. The one the compiler generates for you that kicks off all of the application execution. The one that had to call into your top-most async method to RunAsync. And it had to be public static void Main(string[] args) or public static int Main(). That is not an async method.

Luckily, with the first ever minor language update, C# 7.1, we were given the mighty and powerful async main!

// You can finaly use Task<int>!
public static async Task<int> Main()
{
...
}

// Also available in no return value flavour!
public static async Task Main()
{
...
}

Enter fullscreen mode Exit fullscreen mode

Async and Tests

We have a tonne of async methods in our system. But we should also be testing that code. And our testing frameworks were synchronous.

Luckily our testing frameworks have finally caught up, and with XUnit we can write tests that return Task and are async, and we also have theIAsyncLifetime` interface.

`
// This is what we had to do to test async
public class MyTestCase
{
private readonly MyClass _systemUnderTest;

public MyTestCase()
{
    _systemUnderTest = MyClass();

    Task.Run(async () => await _systemUnderTest.Init()).GetAwaiter().GetResult();
}

public void CanRunSuccessfully()
{

    var result = Task.Run(async () => await _systemUnderTest.Run()).GetAwaiter().GetResult();

    Assert.True(result);
}
Enter fullscreen mode Exit fullscreen mode

}

`

Using Task.Run and GetAwaiter or other synchronising methods can be very error-prone, and prone to deadlocks. Avoiding these is the best approach always (though still not always avoidable).

`
The best way to tet with async
public class MyTestCase : IAsyncLifetime
{
private readonly MyClass _systemUnderTest;

public MyTestCase()
{
    _systemUnderTest = MyClass();
}

public async Task InitializeAsync()
{
    await _systemUnderTest.Init();
}

public Task DisposeAsync()
{
    return Task.CompletedTask;
}

public async Task CanRunSuccessfully()
{

    var result = await _systemUnderTest.Run();

    Assert.True(result);
}
Enter fullscreen mode Exit fullscreen mode

}

`

I'm not sure if other test runners are able to handle async as well as XUnit does, so I always just use XUnit.

ValueTask

The original implementation of the async/await language feature was strongly tied to the Task and `Task types.

In C# 7, the language feature was enhanced, similar to other features, to use a pattern based on method signatures (like the Add for the initialiser syntax). Specifically, the GetAwaiter method must be available on the type used with the await keyword.

Along with this change was the introduction of a new type to leverage this pattern, ValueTask. A ValueTask is a value type (struct) that will be stack-allocated and copied by value. If your method uses caching, and most of the time returns a simple value instead of an awaited execution, the ValueTask may be more efficient than the Task type.

This is because the overheads of Heap-allocation of the reference type Task can have an impact on performance. If you detect this as an issue, you can use the new ValueTask instead. This will be a stack-allocated value type containing the response value and copied around.

Guidance for this: if you mostly return a value, but occasionally call an actual asynchronous IO execution, ValueTask will probably add value (e.g. heavy result caching). If you actually await most of the time, Task should be fine. As usual, measure and test before making the change arbitrarily.

Conclusion

Async programming is becoming the canonical way of building apps with IO in most languages. And in practice, most of the apps I write are IO-bound apps. C# and dotnet make this simple with async/await and the language keeps improving our experience using this successfully. This is a must-use feature that is unavoidable, but knowing the limitations and extensions available to use it well is still very important to do. Use it, but make sure you know enough about how it works to use it well.

Top comments (0)