Search code examples
c#async-awaitvaluetask

What is the point of ValueTask.Preserve()?


ValueTask and ValueTask<TResult> have a Preserve() method which is summarized as "Gets a ValueTask that may be used at any point in the future."

What does that mean and when should we use it? Does it imply that a 'normal' ValueTask can't be used "at any point in the future"? If so, why?


Solution

  • The ValueTask is a performance optimization over a Tasks, but this performance comes with a cost: You cannot use a ValueTask as freely as a Task. The documentation mentions these restrictions:

    The following operations should never be performed on a ValueTask instance:

    • Awaiting the instance multiple times.
    • Calling AsTask multiple times.
    • Using more than one of these techniques to consume the instance.

    These restrictions apply only for ValueTasks that are backed by a IValueTaskSource. A ValueTask can also be backed by a Task, or by a TResult value (for a ValueTask<TResult>). If you know with 100% certainty that a ValueTask is not backed by an IValueTaskSource, you can use it as freely as a Task. For example you can await is multiple times, or you can wait it synchronously to complete with .GetAwaiter().GetResult().

    In general you don't know how a ValueTask is implemented internally, and even if you know (by studying the source code) you may not want to rely on an implementation detail. With the ValueTask.Preserve method you can create a new ValueTask that represents the original ValueTask, and can be used without restrictions because it's not baked by an IValueTaskSource. This method affects the original ValueTask only if it's backed by an IValueTaskSource, otherwise it's a no-op (it just returns the original task unaltered).

    ValueTask preserved = originalValueTask.Preserve();
    

    After calling Preserve, the original ValueTask has been consumed. It should no longer be awaited, or Preserved again, or converted to Task with the AsTask method. Doing any of those actions is likely to result in an InvalidOperationException. But now you have the preserved representation of it, which can be used with the same freedom as a Task.


    My answer included initially a practical example of using the Preserve method, which proved to be incorrect. This example can be found in the 4th revision of this answer.