Click here to Skip to main content
14,669,597 members
Articles » Platforms, Frameworks & Libraries » .NET Framework » How To
Article
Posted 24 Jul 2020

Stats

9.4K views
146 downloads
41 bookmarked

How to Use the C# Await Keyword on Anything

Rate this:
4.91 (27 votes)
Please Sign up or sign in to vote.
4.91 (27 votes)
24 Jul 2020MIT
Using await in scenarios where you want to await custom items with or without using Task.Run()
Await can be used on things other than Task objects - it's simply a matter of implementing the members the compiler wants. Here, I endeavor to explain how to await anything using custom awaitable objects and also using TaskAwaiter.

Introduction

The async/await keywords are a powerful way to add asynchronicity to your applications, improving performance and responsiveness. If you know how to do it, you can make your own awaitable members and types and take advantage of awaiting on anything you like. Here, we peek behind the scenes at await and walk through a couple of different methods for making something awaitable, plus how to choose which one is appropriate.

Conceptualizing this Mess

While I was creating an awaitable socket library I learned a little bit about how to extend awaitable features to things other than Tasks. Thanks to some kind folks here, I happened upon this article by Stephen Toub, who I follow anyway. It explains what I'm about to, but he's very brief about it, and he writes his articles for an advanced audience which makes for a challenging read. We're going to revisit some of his code, and I'll endeavor to make it more accessible to a wider audience of developers.

Awaitable Types

An awaitable type is one that includes at least a single instance method called GetAwaiter() which retrieves an instance of an awaiter type. These can also be implemented as extension methods. Theoretically, you could make an extension method for say, int and make it return an awaiter that represents the current asynchronous operation, such as delaying by the specified integer amount. Using it would be like await 1500 to delay for 1500 milliseconds. We'll be doing exactly that later. The point is that anything that implements GetAwaiter() (either directly or via an extension method) and returns an awaiter object can be awaited on. Task exposes this, and it's the reason a task can be awaited on.

Awaiter Types

The type that GetAwaiter() returns must implement System.Runtime.CompilerServices.INotifyCompletion or the corresponding ICriticalNotifyCompletion interfaces. In addition to implementing the interface's OnCompleted() method, it must also implement two members, called IsCompleted and GetResult() that aren't part of any interface.

The TaskAwaiter

TaskAwaiter exposes all of the awaiter object members, and can be returned from Task. Sometimes, we'll be starting a new task and returning its awaiter in order to simplify things. However, since it's only returned by Task, we can't use it to return things not associated with a task. If you want to make something awaitable that does not use Task to perform its work, you must create your own awaiter object.

Let's get to the code!

Coding this Mess

The Simple Case: Using TaskAwaiter

On an static class, we can implement the following extension method:

internal static TaskAwaiter GetAwaiter(this int milliseconds)
    => Task.Delay(milliseconds).GetAwaiter();

Now you can perform an await on an int and it will wait for the specified number of milliseconds. Remember, anything that starts a Task (like Task.Delay() does) can be used this way. Like I said though, if your operation does not spawn a task whose awaiter you can return, you must implement your own awaiter. Let's look at another example similar to that of the above - this one from Stephen Toub:

public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
{
    return Task.Delay(timeSpan).GetAwaiter();
}

You can see this does the same thing, except for with TimeSpan instead of int, meaning you can await a TimeSpan instance too. You don't have to use extension methods if you can put the GetAwaiter() method directly on your type, in which case it shouldn't be static. Doing this will make your type awaitable just like the extension methods do for other types.

Now we can do:

await 1500; // wait for 1500ms

and:

await new TimeSpan(0, 0, 0, 2); // wait for 2 seconds

I don't actually recommend awaiters on most simple types because it's vague. What I mean is await 1500 says nothing about what it does, and that makes it harder to read. I feel the same with awaiting on a TimeSpan. This code is here to illustrate the concept. With the next bit of code, we'll produce something a little more realistic.

The Not So Simple Case: Creating a Custom Awaiter Type

Sometimes, it doesn't make sense to spawn a Task to fulfill an operation. This can be the case if you're wrapping an asynchronous programming pattern that doesn't use Task. It can also be the case if your operation itself is simple. If you use all struct types for your awaitable type and/or your awaiter type, they will avoid heap allocation. As far as I'm told, running a Task requires at least one object to be allocated on the managed heap. Furthermore, a Task is simply complicated because it needs to be all things to all people. What we really want is a slim way to await.

In this case, we need to create an object implementing one of two interfaces: INotifyCompletion or INotifyCriticalCompletion. The latter does not copy the execution context, which means its potentially faster, but very dangerous as it can elevate code's privilege. Normally, you'll want to use the former, as the risks to code access security usually outweigh any performance gain. The single method, OnCompleted() gets called when the operation completes. This is where you would do any continuation. We'll get to that. Note that OnCompleted() should be public to avoid the framework boxing your struct, which it must do to access the interface. Boxing causes a heap allocation. If the method is public however, it can skip the boxing and access the method directly, I believe. I haven't dived into the IL to verify it yet, but it's not unlikely so this way we can handle that scenario efficiently.

We must also implement IsCompleted and GetResult() which aren't part of any actual interface. The compiler generates code to call these methods, so it's not a runtime thing where interfaces or abstract classes would be the only way. The compiler doesn't need to access things through interfaces because there's no binary contract involved. It's simply that the compiler is generating the code to call the method at the source level, not having to resolve the call by calling through the interface's vtable (the list of function pointers that point to methods for an object in .NET) at runtime. I hope that's clear, but if it's a little confusing don't worry, as it's not important to understand this detail fully in order to use this technique.

In case it's not totally obvious, the IsCompleted property indicates whether or not the operation has been completed.

The GetResult() method takes no arguments and the return type is the same return type of your pseudo-task's result. It can be void if it has no result. If this were the equivalent of a Task<int> you'd return int here. I hope that makes sense. This is where you want to do the primary work for the task. This method blocks, meaning your code can be synchronous. If retrieving the result failed (meaning the operation failed), this method is where you'd throw.

I was trying to think of a good use case for creating your own awaiter that wasn't too complex, and was having a difficult time of it. Fortunately, Sergey Tepliakov produced a fine example here in which I only had to modify OnCompleted() and IsCompleted a little bit. We'll explore it below:

// modified from 
// https://devblogs.microsoft.com/premier-developer/extending-the-async-methods-in-c/
// premodifed source by Sergey Tepliakov
static class LazyUtility
{
    // our awaiter type
    public struct Awaiter<T> : INotifyCompletion
    {
        private readonly Lazy<T> _lazy;
            
        public Awaiter(Lazy<T> lazy) => _lazy = lazy;

        public T GetResult() => _lazy.Value;

        public bool IsCompleted => _lazy.IsValueCreated;

        public void OnCompleted(Action continuation)
        {
            // run the continuation if specified
            if (null != continuation)
                Task.Run(continuation);
        }
    }
    // extension method for Lazy<T>
    // required for await support
    public static Awaiter<T> GetAwaiter<T>(this Lazy<T> lazy)
    {
        return new Awaiter<T>(lazy);
    }
}

This extends Lazy<T> to be awaitable. All the work is done by Lazy<T> when we call its Value property in GetResult(). This way, if you have a long running initialization, you can complete it asynchronously simply by awaiting your Lazy<T> instance.

Note we're never spawning a Task here. Again, GetResult() can block, like it would here, if your Lazy<T>'s initialization code is long running.

Also note that we take a Lazy<T> argument in the constructor, which is important for obvious reasons.

We're running continuation in OnCompleted() which allows for chaining tasks together with Task.ContinueWith() for example.

You can see that we're also forwarding to IsValueCreated in IsCompleted. This lets the framework know if the work in GetResult() has finished which it has once Lazy<T> creates the value. Note that we're never creating a Task ourselves except when we run the continuation in OnCompleted(). This makes for a more efficient way to do awaiting than creating a Task for it.

Now we can do:

var result = await myLazyT; // awaitable initialization of myLazyT

It should be noted that whatever you do in this class should be thread safe. Lazy<T> already is.

Hopefully, that should get you started creating awaitable objects. Enjoy!

History

  • 24th July, 2020 - Initial submission

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

honey the codewitch
United States United States
Just a shiny lil monster. Casts spells in C++. Mostly harmless.

Comments and Discussions

 
PraiseMy vote of 5 Pin
Chris Copeland27-Jul-20 22:37
professionalChris Copeland27-Jul-20 22:37 
Fascinating, I had no idea you could do something like this Thumbs Up | :thumbsup:

GeneralRe: My vote of 5 Pin
honey the codewitch27-Jul-20 22:39
mvahoney the codewitch27-Jul-20 22:39 
PraiseNoice Pin
Sander Rossel26-Jul-20 23:58
professionalSander Rossel26-Jul-20 23:58 
GeneralRe: Noice Pin
honey the codewitch27-Jul-20 0:08
mvahoney the codewitch27-Jul-20 0:08 
GeneralRe: Noice Pin
Sander Rossel27-Jul-20 0:16
professionalSander Rossel27-Jul-20 0:16 
GeneralRe: Noice Pin
honey the codewitch27-Jul-20 0:25
mvahoney the codewitch27-Jul-20 0:25 
GeneralRe: Noice Pin
Sander Rossel27-Jul-20 0:30
professionalSander Rossel27-Jul-20 0:30 
GeneralRe: Noice Pin
honey the codewitch27-Jul-20 4:21
mvahoney the codewitch27-Jul-20 4:21 
GeneralMy vote of 5 Pin
Matthew Dennis26-Jul-20 10:35
sysadminMatthew Dennis26-Jul-20 10:35 
GeneralRe: My vote of 5 Pin
honey the codewitch26-Jul-20 12:44
mvahoney the codewitch26-Jul-20 12:44 
GeneralMy vote of 5 Pin
Franc Morales25-Jul-20 10:13
MemberFranc Morales25-Jul-20 10:13 
GeneralRe: My vote of 5 Pin
honey the codewitch25-Jul-20 10:16
mvahoney the codewitch25-Jul-20 10:16 
GeneralMy vote of 5 Pin
0x01AA25-Jul-20 3:16
mve0x01AA25-Jul-20 3:16 
GeneralRe: My vote of 5 Pin
honey the codewitch25-Jul-20 3:38
mvahoney the codewitch25-Jul-20 3:38 
GeneralMy vote of 5 Pin
LightTempler24-Jul-20 21:48
MemberLightTempler24-Jul-20 21:48 
GeneralRe: My vote of 5 Pin
honey the codewitch25-Jul-20 2:12
mvahoney the codewitch25-Jul-20 2:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.