The corefxlab repository contains library suggestions for corefx which itself is a repository containing the .NET Core foundational libraries. One of the gems hidden among these libraries is ValueTask<T>
that was added by Stephen Toub as part of the System.Threading.Tasks.Channels
library but may be extremely useful on its own. The full implementation of ValueTask<T>
can be found here, but this is an interesting subset of the API:
public struct ValueTask<TResult>
{
public ValueTask(TResult result);
public ValueTask(Task<TResult> task);
public static implicit operator ValueTask<TResult>(Task<TResult> task);
public static implicit operator ValueTask<TResult>(TResult result);
public Task<TResult> AsTask();
public bool IsCompletedSuccessfully { get; }
public TResult Result { get; }
public ValueTaskAwaiter GetAwaiter();
public ValueTaskAwaiter ConfigureAwait(bool continueOnCapturedContext);
// ...
}
I first noticed ValueTask<T>
in the API documentation when reviewing the channels PR made to corefxlab. I suggested adding a short explanation which Stephen quickly provided:
“
ValueTask<T>
is a discriminated union of aT
and aTask<T>
, making it allocation-free forReadAsync<T>
to synchronously return aT
value it has available (in contrast to usingTask.FromResult<T>
, which needs to allocate aTask<T>
instance).ValueTask<T>
is awaitable, so most consumption of instances will be indistinguishable from with aTask<T>
.”
ValueTask
, being a struct
, enables writing async methods that do not allocate memory when they run synchronously without compromising API consistency. Imagine having an interface with a Task
returning method. Each class implementing this interface must return a Task
even if they happen to execute synchronously (hopefully using Task.FromResult
). You can of course have 2 different methods on the interface, a synchronous one and an async one but this requires 2 different implementations to avoid “sync over async” and “async over sync”.
ValueTask<T>
has implicit casts from both T
and Task<T>
(EDIT: The implicit casts were removed to prepare for arbitrary async returns) and can be awaited by itself which makes it extremely simple to use. Consider this possibly async provider API returning a ValueTask<T>
:
interface IHamsterProvider
{
ValueTask<Hamster> GetHamsterAsync(string name);
}
This provider interface can be implemented synchronously (for in-memory hamsters) by performing a lookup in a Dictionary
and returning the Hamster
instance (which is implicitly converted into a ValueTask<Hamster>
without any additional allocations):
class LocalHamsterProvider : IHamsterProvider
{
ConcurrentDictionary<string, Hamster> _dictionary; // ...
public ValueTask<Hamster> GetHamsterAsync(string name)
{
Hamster hamster = _dictionary[name];
return hamster;
}
}
Or asynchronously (for hamsters stored in MongoDB) by performing an asynchronous query and returning the Task<Hamster>
instance (which is implicitly converted into ValueTask<Hamster>
as well):
class MongoHamsterProvider : IHamsterProvider
{
IMongoCollection<Hamster> _collection; // ...
public ValueTask<Hamster> GetHamsterAsync(string name)
{
Task<Hamster> task = _collection.Find(_ => _.Name == name).SingleAsync();
return task;
}
}
The consumer of this API can await the ValueTask<Hamster>
as if it was a Task<Hamster>
without knowing whether it was performed asynchronously or not with the benefit of no added allocations:
Hamster hamster = await Locator.Get<IHamsterProvider>().GetHamsterAsync("bar");
While this example shows 2 different implementations, one synchronous and the other asynchronous, they could easily be combined. Imagine a provider using a local cache for hamsters and falling back to the DB when needed.
The few truly asynchronous calls to GetHamsterAsync
would indeed require allocating a Task<Hamster>
(which will be implicitly converted to ValueTask<Hamster>
) but the rest would complete synchronously and allocation-free:
class HamsterProvider : IHamsterProvider
{
ConcurrentDictionary<string, Hamster> _dictionary; // ...
IMongoCollection<Hamster> _collection; // ...
public ValueTask<Hamster> GetHamsterAsync(string name)
{
Hamster hamster;
if (_dictionary.TryGetValue(name, out hamster))
{
return hamster;
}
Task<Hamster> task = _collection.Find(_ => _.Name == name).SingleAsync();
task.ContinueWith(_ => _dictionary.TryAdd(_.Result.Name, _.Result));
return task;
}
}
This kind of “hybrid” use of async-await is very common, since these outbound operations to a data store, remote API, etc. can usually benefit from some kind of caching.
It’s hard to quantify how much of an improvement widespread usage of ValueTask
can bring as it greatly depends on the actual usage but avoiding allocations not only saves the allocation cost but also greatly reduces the garbage collection overhead. That’s why it was requested to be added to .NET Core regardless of the channels library.