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.
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.
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.
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
GetResult() that aren't part of any interface.
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)
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.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)
You can see this does the same thing, except for with
TimeSpan instead of
int, meaning you can
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 new TimeSpan(0, 0, 0, 2);
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
In this case, we need to create an object implementing one of two interfaces:
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
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.
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
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
IsCompleted a little bit. We'll explore it below:
static class LazyUtility
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)
if (null != continuation)
public static Awaiter<T> GetAwaiter<T>(this Lazy<T> lazy)
return new Awaiter<T>(lazy);
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
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.
OnCompleted() which allows for chaining tasks together with
Task.ContinueWith() for example.
You can see that we're also forwarding to
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
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;
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!
- 24th July, 2020 - Initial submission
Just a shiny lil monster. Casts spells in C++. Mostly harmless.