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();     } } 
Add Comment
2 Answer(s)

There are pros and cons of each. In the "pro" column:

  1. When returning a result synchronously (i.e. Task<T>), using ValueTask<T> avoids an allocation of the task – however, this doesn’t apply for "void" (i.e. non-generic Task), since you can just return Task.CompletedTask
  2. 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.

Add Comment

ValueTask is a struct, whereas Task is a class that’s why it might reduce the allocations. (structs are allocated most of the time on the stack, so they will vanish automatically when the method exits (exiting the strackframe). classes 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.

Answered on July 16, 2020.
Add Comment

Your Answer

By posting your answer, you agree to the privacy policy and terms of service.