Task vs ValueTask and IValueTaskSource

 
 
  • Gérald Barré

This post is part of the series 'Async/await in C#'. Be sure to check out the rest of the blog posts of the series!

ValueTask<T> is the return type people reach for when they have heard that Task allocates, and then use everywhere. That is the wrong lesson. ValueTask solves one specific problem, it solves it very well, and it comes with rules that Task does not have.

#What it is

ValueTask<T> is a struct holding one of three things:

  • a TResult directly, for a synchronous success,
  • a Task<TResult>, for the asynchronous case,
  • an IValueTaskSource<TResult> plus a version token, for the pooled case.

The first is the interesting one. When your method already has the answer, ValueTask<T> carries it back without touching the heap. Task<T> cannot: even Task.FromResult has to hand back an object.

That single difference is the entire justification for the type.

#When it pays off

The benchmark models the scenario it was designed for: a lookup that usually hits a cache and occasionally has to fetch.

MethodHitRateMeanRatioAllocatedAlloc Ratio
TaskBased014.724 ns1.00184 B1.00
ValueTaskBased012.362 ns0.84112 B0.61
TaskBased5011.722 ns1.00128 B1.00
ValueTaskBased508.826 ns0.7556 B0.44
TaskBased1007.403 ns1.0072 B1.00
ValueTaskBased1004.904 ns0.66-0.00
Benchmark project, measured with BenchmarkDotNet

At a 100% hit rate the ValueTask version allocates nothing at all, against 72 bytes per call for Task. At 50% it halves the allocations. Even at 0%, where every call goes down the asynchronous path, it still saves the 72-byte wrapper Task that the async Task<string> method would have produced.

That last row deserves emphasis, because the folklore says ValueTask is worse when you always go async. It is not worse here. What is true is that once the method genuinely suspends, the difference stops mattering, because scheduling dominates.

So the rule is about shape, not about speed:

  • Use ValueTask<T> when the method often returns without awaiting anything, and it is called often enough for 72 bytes to matter.
  • Use Task<T> everywhere else, which is most places.

Task is still the better default. It composes with everything, it can be awaited repeatedly, it can be stored, and nobody has to think about it.

#The rules

A ValueTask backed by an IValueTaskSource is only valid until you consume it, because the source recycles its state afterwards. That leads to three restrictions:

Await it once. A second await reads state that may now belong to another operation. The runtime throws.

Do not block on it. .Result and .GetAwaiter().GetResult() on a ValueTask that has not completed are invalid, not merely slow. Unlike Task, there is nothing to block on.

Do not store it, pass it around, or hand it to WhenAll. Convert first.

C#
// Need it more than once, or need to compose? Convert.
Task<string> task = GetValueTaskAsync().AsTask();
await Task.WhenAll(task, other);

default(ValueTask<T>) is worth knowing about separately: it is a completed task with the default result, where default(Task<T>) is null. That makes it a convenient "nothing to do" return value.

#Building one: IValueTaskSource

The third form is where the real savings are. An IValueTaskSource lets a single long-lived object hand out many ValueTasks without allocating for any of them. This is how Socket, NetworkStream and System.IO.Pipelines avoid a Task per read.

The sample builds an awaitable manual reset event on top of ManualResetValueTaskSourceCore<T>, which does the hard parts for you:

C#
public sealed class AsyncManualResetEvent : IValueTaskSource
{
    private ManualResetValueTaskSourceCore<bool> _core = new() { RunContinuationsAsynchronously = true };
    private bool _isSet;

    public ValueTask WaitAsync()
    {
        lock (this)
        {
            if (_isSet)
                return ValueTask.CompletedTask;

            return new ValueTask(this, _core.Version);
        }
    }

    public void Set()
    {
        lock (this)
        {
            if (_isSet)
                return;

            _isSet = true;
            _core.SetResult(true);
        }
    }

    void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
    ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token);
    void IValueTaskSource.OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
        => _core.OnCompleted(continuation, state, token, flags);
}

The three interface members map exactly onto the awaiter pattern from part 4: GetStatus is IsCompleted, GetResult is GetResult, OnCompleted is OnCompleted. The only addition is the short token.

That token is the whole safety mechanism. _core.Version increments on every Reset, and every operation checks the token it was given against the current version. Hand out a ValueTask, reset the core, then await the stale ValueTask, and it throws rather than returning somebody else's result. The sample's tests check exactly that.

Compare that to the obvious TaskCompletionSource implementation, which allocates a TaskCompletionSource and a Task per wait. For an event signalled millions of times, that is the difference between a steady allocation rate and none.

#When to write one

Rarely, and only after measuring. IValueTaskSource is a performance tool for library authors, with real complexity: manual token management, thread safety you have to get right, and failure modes that surface as "the wrong result appeared in an unrelated operation".

The cases that justify it look like this:

  • One object completes an operation many times over its lifetime. A socket read, a channel read, an event.
  • The operation frequently completes synchronously, so the savings are the whole allocation rather than part of it.
  • It is on a genuinely hot path, established by profiling rather than intuition.

If those do not all hold, TaskCompletionSource is clearer and fast enough. And if you only need a pooled state machine for an ordinary async method, [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] gets you most of the way with none of the code.

#What to take away

  • ValueTask<T> is a struct wrapping a result, a Task<T>, or an IValueTaskSource<T> plus a token.
  • It pays off when the method often returns without suspending. At a 100% cache hit rate it allocates nothing.
  • Even when it always goes async it is not worse, but the advantage stops mattering once scheduling dominates.
  • Await it once, never block on it, never store it. Use AsTask() when you need any of that.
  • default(ValueTask<T>) is a completed task, unlike default(Task<T>).
  • IValueTaskSource with ManualResetValueTaskSourceCore<T> lets one object hand out many awaitables with no allocation, and the version token is what makes misuse throw instead of corrupt.

The next post applies all of this to sequences: IAsyncEnumerable<T> and await foreach, which combine the state machine with an iterator and return ValueTask<bool> for exactly the reasons above.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?