Since async/await was added to C# 5.0 you could await
any custom awaitable type as long as it follows a specific pattern: has a GetAwaiter
method that returns an awaiter that in turn has IsCompleted
, OnCompleted
and GetResult
(more on it here). But the language is stricter when it comes to the return type of an async method. You can only return 3 types from an async method: void
, Task
and Task<T>
.
Task
is for async methods that don’t have a result (i.e. procedures) while Task<T>
is for async methods that return a result (i.e. functions). That leaves async void
methods which mainly exist for backwards-compatibility with event handlers (the void Button_Click(object sender, EventArgs e)
kind) and should be avoided elsewhere as unhandled exceptions inside them will crash the entire process.
These compiler rules were recently expanded to also allow returning any custom type from an async method as long as it follows a specific pattern. For example if you have a custom HardTask<T>
type for these extra-hard tasks, in order to return it from an async method you would need:
HardTask<T>
. It’s very similar to AsyncTaskMethodBuilder<T>
but the main difference is that the Task
property returns the custom HardTask<T>
instead of Task<T>
:struct HardTaskMethodBuilder<TResult>
{
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine { ... }
public void SetStateMachine(IAsyncStateMachine stateMachine) { ... }
public void SetResult(TResult result) { ... }
public void SetException(Exception exception) { ... }
// Returns HardTask<TResult>, not Task<TResult>
public HardTask<TResult> Task { get; }
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter,
ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine { ... }
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter,
ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine { ... }
}
AsyncMethodBuilderAttribute
on the custom type specifying the method builder to be used with it:[AsyncMethodBuilder(typeof(HardTaskMethodBuilder<>))]
class HardTask<TResult>
{
...
}
The main driver behind this feature is performance as it will enable easier use of ValueTask<T>
(which already supports this feature). ValueTask<T>
is a discriminated union of T
for synchronous cases and Task<T>
for asynchronous ones. By being a struct
it keeps the synchronous case allocation-free and reduces GC pressure. This is useful for operations that usually complete synchronously but occasionally run asynchronously like async streams or async collections but the best example in my opinion are caches.
When using a cache most results are returned synchronously from the cache but for the few cache-misses you usually need to do some I/O asynchronously. Before ValueTask<T>
you could either create a new Task<T>
instance in the synchronous case with Task.FromResult
(which the GC now needs to collect) or complicate the code and cache the Task
objects themselves. By returning a ValueTask<T>
you can wrap a T
result if it’s available or a Task<T>
if you need to execute an asynchronous operation to get it. Implementing this behavior is much easier with the help of the async/await “compiler magic”. For example this HamsterProvider
that holds a cache of Hamster
objects returned from MongoDB:
class HamsterProvider
{
ConcurrentDictionary<string, Hamster> _dictionary; // ...
IMongoCollection<Hamster> _hamsters; // ...
public ValueTask<Hamster> GetHamsterAsync(string name)
{
Hamster hamster;
if (_dictionary.TryGetValue(name, out hamster))
{
// Return synchronously from cache
return new ValueTask<Hamster>(hamster);
}
// Kick off the asynchronous query
Task<Hamster> task = _hamsters.Find(_ => _.Name == name).SingleOrDefaultAsync();
// Add a continuation to cache the result after the query completed
Task<Hamster> continuation = task.ContinueWith(completedTask =>
{
Hamster result = completedTask.Result;
if (result == null)
{
throw new Exception($"Hamster named {result.Name} does not exist.");
}
_dictionary.TryAdd(result.Name, result);
return result;
});
// Return the not-yet-completed continuation for the caller to await
return new ValueTask<Hamster>(continuation);
}
}
Can be refactored into this much more readable code using async/await instead of Task.ContinueWith
without adding any allocations or degrading performance:
class HamsterProvider
{
ConcurrentDictionary<string, Hamster> _dictionary; // ...
IMongoCollection<Hamster> _hamsters; // ...
public async ValueTask<Hamster> GetHamsterAsync(string name)
{
Hamster hamster;
if (_dictionary.TryGetValue(name, out hamster))
{
// Return synchronously from cache
return hamster;
}
// Kick off and await the asynchronous query
hamster = await _hamsters.Find(_ => _.Name == name).SingleOrDefaultAsync();
if (hamster == null)
{
throw new Exception($"Hamster named {hamster.Name} does not exist.");
}
// Cache the result
_dictionary.TryAdd(hamster.Name, hamster);
// Return the result
return hamster;
}
}
While this feature is geared towards ValueTask<T>
it’s not limited to it. You can use it with any type you like as long as you implement the builder correctly. For example you can create a task that adds a log before each await
, support WinRT’s IAsyncAction
(when C# adds extension static methods) or even implement the Maybe monad as Chad Kimes did with NullableTaskLike<T>
.
This feature was merged into the dev15-preview-4
branch so I assume this will show up in the next Visual Studio 15 preview (not to be confused with VS 2015) and end up in C# 7.0 when it’s released. If you want to go deeper you can take a look at Lucian Wischik’s feature proposal and Charles Stoner’s implementation PR.