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?
CodePudding user response:
The documentation isn't very clear. From the source :
public ValueTask Preserve() => _obj == null ? this : new ValueTask(AsTask());
So it's effectively just making a clone of the object in the case that the ValueTask
represents a Task
or an IValueTaskSource
(_obj != null
) or otherwise just copies by assignment if the ValueTask
represents a successful synchronous completion. It's necessary because _obj
is internal and can't be tested externally.
/// <summary>null if representing a successful synchronous completion,
/// otherwise a <see cref="Task"/> or a <see cref="IValueTaskSource"/>.</summary>
internal readonly object? _obj;
The warning at the top gives a bit more information :
// TYPE SAFETY WARNING:
// This code uses Unsafe.As to cast _obj. This is done in order to minimize the costs associated with
// casting _obj to a variety of different types that can be stored in a ValueTask, e.g. Task<TResult>
// vs IValueTaskSource<TResult>. Previous attempts at this were faulty due to using a separate field
// to store information about the type of the object in _obj; this is faulty because if the ValueTask
// is stored into a field, concurrent read/writes can result in tearing the _obj from the type information
// stored in a separate field. This means we can rely only on the _obj field to determine how to handle
// it. As such, the pattern employed is to copy _obj into a local obj, and then check it for null and
// type test against Task/Task<TResult>. Since the ValueTask can only be constructed with null, Task,
// or IValueTaskSource, we can then be confident in knowing that if it doesn't match one of those values,
// it must be an IValueTaskSource, and we can use Unsafe.As. This could be defeated by other unsafe means,
// like private reflection or using Unsafe.As manually, but at that point you're already doing things
// that can violate type safety; we only care about getting correct behaviors when using "safe" code.
// There are still other race conditions in user's code that can result in errors, but such errors don't
// cause ValueTask to violate type safety.
CodePudding user response:
The ValueTask
is a performance optimization over a Task
s, 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 ValueTask
s that are backed by a IValueTaskSource
. ValueTask
s 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 a IValueTaskSource
, you can use it as freely as a Task
. For example you can await
is multiple times, or you can wait synchronously for its completion 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. The ValueTask.Preserve
method is a shortcut for creating a new ValueTask
that represents the current ValueTask
, and is backed by a Task
. The two lines below are roughly equivalent:
ValueTask preserved = myValueTask.Preserve();
ValueTask preserved = new ValueTask(myValueTask.AsTask());
After calling Preserve
the original ValueTask
has been consumed, and can no longer be awaited, or Preserve
d 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
.
Here is a practical example where the ValueTask.Preserve
can be useful. Let's assume that you want to create a LINQ operator for IAsyncEnumerable<T>
sequences. The enumerator has a MoveNextAsync
method that returns a ValueTask<bool>
. You want to perform some logic after calling this method and before awaiting it, that might throw an exception.
Now you have the problem that you are not allowed to dispose the enumerator while a MoveNextAsync
operation is in flight. You must await
any pending MoveNextAsync
operation before disposing the enumerator, otherwise an InvalidOperationException
might be thrown by the DisposeAsync
. But when you are in the finally
block, you can't be sure if the last MoveNextAsync
operation has already been awaited. Awaiting it a second time may again cause an InvalidOperationException
.
One solution to this problem is to convert the ValueTask<bool>
to a Task<bool>
immediately after you get it from the MoveNextAsync
. But then you are allocating memory invariably on each iteration, even if the ValueTask<bool>
is already completed upon creation, and you expect the vast majority of the ValueTask<bool>
s to be like this. In these very specific circumstances you could use the Preserve
method to your advantage like this:
static async IAsyncEnumerable<T> MyOperator<T>(this IAsyncEnumerable<T> source)
{
var enumerator = source.GetAsyncEnumerator();
ValueTask<bool> moveNext = default;
try
{
while (true)
{
moveNext = enumerator.MoveNextAsync();
if (moveNext.IsCompletedSuccessfully)
moveNext = new ValueTask<bool>(moveNext.Result);
else
moveNext = moveNext.Preserve();
// Let's assume that here there is complex logic that might throw
if (!await moveNext) break;
// Here there is more logic that might throw
yield return enumerator.Current;
}
}
finally
{
if (!moveNext.IsCompleted) try { await moveNext; } catch { }
await enumerator.DisposeAsync();
}
}
This way you are paying the cost of allocating a Task<bool>
only when the ValueTask<bool>
completes asynchronously (or synchronously but unsuccessfully).
The above example also indicates that the Preserve
method is not optimized enough to avoid the allocation of a Task
, in case the ValueTask
is backed by a IValueTaskSource
and also has already completed successfully. That's the reason for checking the moveNext.IsCompletedSuccessfully
condition. Admittedly this check is almost certainly superficial in this case. It's extremely unlikely that a ValueTask
which is completed upon creation, is backed by a IValueTaskSource
.