Should I return ValueTask if I still use TaskCompletionSource to implement asynchrony?
Is there still a benefit in returning ValueTask
if I use TaskCompletionSource
to implement actual asynchrony?
As I understand it, the purpose of ValueTask
is to reduce allocations, but allocations are still there when awaiting TaskCompletionSource.Task
. Here is a simple example to illustrate the question. Should I rather be returning Task
(versus ValueTask
) from DispatcherTimerDelayAsync
, which itself is always expected to be asynchronous?
async ValueTask DispatcherTimerDelayAsync(TimeSpan delay) { var tcs = new TaskCompletionSource<bool>(); var timer = new DispatcherTimer(); timer.Interval = delay; timer.Tick += (s, e) => tcs.SetResult(true); timer.Start(); try { await tcs.Task; } finally { timer.Stop(); } }
There are pros and cons of each. In the "pro" column:
- When returning a result synchronously (i.e.
Task<T>
), usingValueTask<T>
avoids an allocation of the task – however, this doesn’t apply for "void" (i.e. non-genericTask
), since you can just returnTask.CompletedTask
- When you are issuing multiple sequential awaitables, you can use a single "value task source" with sequential tokens to reduce allocations
(a special-case of "2" might be amortization via tools like PooledAwait
)
It doesn’t feel like either of those apply here, but: if in doubt, remember that it is allocation-free to return a Task
as a ValueTask
(return new ValueTask(task);
), which would allow you to consider changing the implementation to an allocation-free one later without breaking the signature. You still pay for the original Task
etc of course – but exposing them as ValueTask
doesn’t add any extra.
In all honesty, I’m not sure I’d worry too much about this in this case, since a delay is ways going to be allocatey.
ValueTask
is a struct, whereas Task
is a class that’s why it might reduce the allocations. (struct
s are allocated most of the time on the stack, so they will vanish automatically when the method exits (exiting the strackframe). class
es are allocated mostly on the heap, where a GC Collect or Sweep has to be performed to gather unreachable objects. So if you can allocate on a stack then your heap memory allocation is not growing (because of this object) that’s the GC is not triggered.)
The main benefit of ValueTask
can be seen when your code executes mainly synchronously. It means that if the expected value is already present then there is no need to create a promise (TaskCompletionSource
), which will be fulfilled in the future.
You also don’t need to worry about allocation in case of Task<bool>
because in both cases the corresponding Task objects are cached by the runtime itself. The same applies for Task<int>
but only for numbers between -1 and 9. Reference
So in short, you should use Task
in this case instead of ValueTask
.