Task vs Value Task

Task vs ValueTask

Task

Task serves multiple purposes, but at its core it’s a “promise”, an object that represents the eventual completion of some operation. Task as a class is very flexible and has the following benefits:

  • One can await it multiple times, by any number of consumers concurrently
  • One can store one into a dictionary for any number of subsequent consumers to await in the future, which allows it to be used as a cache for asynchronous results.
  • One can block waiting for one to complete should the scenario require that.
  • One can write and consume a large variety of operations over tasks, such as a “when any” operation that asynchronously waits for the first to complete.

However, that flexibility is not needed for the most common case: simply invoking an asynchronous operation and awaiting its resulting task, we simply need to be able to await the resulting promise of the asynchronous operation.

Further, Task does have a potential downside, in particular for scenarios where instances are created a lot and where high-throughput and performance is a key concern: Task is a class. As a class, that means that any operation which needs to create one needs to allocate an object, and the more objects that are allocated, the more work the garbage collector (GC) needs to do, and the more resources we spend on it that could be spent doing other things.

There are many cases where operations complete synchronously and are forced to allocate a Task<TResult> to hand back.

ValueTask

ValueTask was introduced in .NET Core 2.0 as a struct capable of wrapping either a TResult or a Task<TResult>. This means it can be returned from an async method, and if that method completes synchronously and successfully, nothing need be allocated: we can simply initialize this ValueTask<TResult> struct with the TResult and return that. Only if the method completes asynchronously does a Task<TResult> need to be allocated.

One example where ValueTask could be used is scenario where one is able to write a async method that can complete synchronously without incurring additional allocation for result type. The scenario could be the following:

public override ValueTask<int> ReadAsync(byte[] buffer, int offset, int count)
{
    try
    {
        int bytesRead = Read(buffer, offset, count);
        return new ValueTask<int>(bytesRead);
    }
    catch (Exception e)
    {
        return new ValueTask<int>(Task.FromException<int>(e));
    }
}

New methods that are expected to be used on hot paths are now defined to return ValueTask<TResult> instead of Task<TResult>

However, because ValueTask and ValueTask<TResult> may wrap reusable objects, there are actually significant constraints on their consumption when compared with Task and Task<TResult>. The following cases should be avoided:

  • Awaiting a ValueTask / ValueTask<TResult> multiple times.
  • Awaiting a ValueTask / ValueTask<TResult> concurrently.
  • Using .GetAwaiter().GetResult() when the operation hasn’t yet completed, instead use IsCompleted , IsCompletedSuccessfully to see if the task is completed or completed successfully or not.
// Given this ValueTask<int>-returning method…
public ValueTask<int> SomeValueTaskReturningMethodAsync();
…
// GOOD
int result = await SomeValueTaskReturningMethodAsync();

// GOOD
int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false);

// GOOD
Task<int> t = SomeValueTaskReturningMethodAsync().AsTask();

// WARNING
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
... // storing the instance into a local makes it much more likely it'll be misused,
    // but it could still be ok

// BAD: awaits multiple times
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
int result = await vt;
int result2 = await vt;

// BAD: awaits concurrently (and, by definition then, multiple times)
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
Task.Run(async () => await vt);
Task.Run(async () => await vt);

// BAD: uses GetAwaiter().GetResult() when it's not known to be done
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
int result = vt.GetAwaiter().GetResult();

So unless performance implications are more important than usability implications, Task<TResult> is preferred in general when compared to ValueTask<TResult>. But that is not the end for ValueTask there are many a scenarios where it might be great choice.

Thanks for stopping by!!! Feel free to comment to this post or drop an email to naik899@gmail.com