Should I return ValueTask if I still use TaskCompletionSource to implement asynchrony?

Viewed 1177

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:

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();
    }
}

Should I rather be returning Task (versus ValueTask) from DispatcherTimerDelayAsync, which itself is always expected to be asynchronous?

2 Answers

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.

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 have 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 why 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.

Related