7

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?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
avo
  • 10,101
  • 13
  • 53
  • 81
  • 5
    Seems to me your method can be simplified to `await Task.Delay(delay);`. Am I understanding it correctly? – Tanveer Badar Jul 08 '20 at 06:06
  • 4
    @Tanveer remove the `async` and just `return Task.Delay(delay);`: even fewer allocs! – Marc Gravell Jul 08 '20 at 06:31
  • 1
    Related reading: https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html – Marc Gravell Jul 08 '20 at 07:06
  • @TanveerBadar, indeed it can, it's a made-up example just to show what I meant. – avo Jul 08 '20 at 07:57
  • 1
    I should have mentioned that, in the real life it is a custom I/O controller which I access via interop and a vendor-provided C++ Win32 DLL, but it still comes down to turning callbacks into Tasks, and I use `TaskCompletionSource` for that. – avo Jul 08 '20 at 08:07

2 Answers2

10

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks, this helps! A great blog post, too! In the real life it is a custom I/O controller which I access via interop and a vendor-provided C++ Win32 DLL, but it still comes down to turning callbacks into Tasks, and I use `TaskCompletionSource` for that. – avo Jul 08 '20 at 08:03
  • Would that be a good idea to implement my own cache of IValueTaskSource-based tasks, using something like `ManualResetValueTaskSourceCore`? – avo Jul 08 '20 at 08:05
  • 1
    @avo if we're talking about sequential (rather than overlapping) operations, and if reducing allocations *is key* (frankly, it often isn't), then yes: that could definitely help. But a lot of times you don't even need a cache as such - if you can simple keep and a the `ManualResetValueTaskSourceCore` as a field on the instance. – Marc Gravell Jul 08 '20 at 08:12
  • I've just gone through your [`PooledAwait`](https://mgravell.github.io/PooledAwait/) and `PooledValueTaskSource` seems to be exactly what I should be looking for. Great work, thank you. I hope TPL will provide something like this in .NET 5 or later. – avo Jul 08 '20 at 08:16
  • 1
    Not in v5, the bits were merged few months ago I believe but the feature is off through a flag in v5 timeline from what I understand. – Tanveer Badar Jul 08 '20 at 10:52
  • @MarcGravell Would an implementation of request response based protocol fit case number 2? For example a protocol that supports windowing, (sending multiple requests before receiving a response, using sequence numbers to match req to resp). – uriDium Feb 18 '22 at 09:30
  • @uriDium it could do, depending on context; you can only have one outstanding result per ivaluetasksource, normally, though - so you might need to be careful if you have overlapped requests - you'd need to abstract the reuse carefully – Marc Gravell Feb 18 '22 at 15:06
  • Yes the protocol does support overlapped requests. I was thinking of maintaining a dictionary of ValueTask keyed on the request's sequence number. I did this easily with TaskCompletionSource quite easily but want to keep down the pressure on the GC. It is quite a high throughput service, but it is still an over optimization but I was doing it for my own interest and understanding. – uriDium Feb 21 '22 at 06:47
  • @uriDium yes, it absolutely can be done; it needs to be done carefully and mindfully; as an example, "PooledAwait" uses a separate pool of reusable IVTS, which means that ordering (overlapped, etc) isn't an issue: it simply tries to take something from the pool, and allocates if it can't - and puts things back into the pool when the result is fetched. – Marc Gravell Feb 21 '22 at 08:16
5

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.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • 2
    This is a great answer, i was so sceptical when i started to read this, but you surprised me. The only thing i would add is, unless your benchmarks and profiling tells you that you are going to benefit from the use of a ValueTask on your hot paths and that the down stream code can abide by its requirements, then you should just use a regular task – TheGeneral Jul 08 '20 at 06:26
  • @TheGeneral First all of thanks. You are absolutely right with the measurement suggestions. Measure before make any long lasting decision. – Peter Csala Jul 08 '20 at 06:33
  • 2
    @TheGeneral the "something accessed in an async way multiple times but sequentially" scenario is also a very compelling candidate for `ValueTask[]` - via the "source+token" approach – Marc Gravell Jul 08 '20 at 06:34
  • 2
    @MarcGravell compelling indeed. 3 things things to live by, Be good to your mother, allocations and thread pool. And always ask yourself "*what would MarcGravell do*" :P – TheGeneral Jul 08 '20 at 06:47
  • 3
    @TheGeneral I'm not sure I'd trust that idiot, but I think he'd [Prefer ValueTask to Task, always](https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html) – Marc Gravell Jul 08 '20 at 07:04
  • @MarcGravell nice find! He has a some good things to say, and well read ;) – TheGeneral Jul 08 '20 at 07:41
  • @MarcGravell, maybe not always but mostly? :) I mean, there could be twisted cases like WhenAll/WhenAny were you'd still want to have a `Task`? Or are you saying that the user of the API should be responsible for that and use ValueTask.AsTask? – avo Jul 08 '20 at 08:20
  • 2
    *"The same applies for `Task` but only for numbers between 0 and 10."* <== is this documented, or you know it by studying the source code? – Theodor Zoulias Jul 08 '20 at 08:22
  • 1
    @TheodorZoulias I read that once in the Pro .NET Memory Management book on page [477](https://books.google.hu/books?id=pIx5DwAAQBAJ&pg=PA477&lpg=PA477&dq=task%3Cint%3E+caching&source=bl&ots=r1JAtelvVq&sig=ACfU3U0VHzC-kAzFNNBCbN5GhiZ-Fhwn5Q&hl=en&sa=X&ved=2ahUKEwjn1IiKnb3qAhXJpIsKHYe9CXwQ6AEwAXoECAwQAQ#v=onepage&q&f=false). I've edited the post because I remembered to a wrong range. – Peter Csala Jul 08 '20 at 08:26
  • 1
    @avo indeed, one should *never ever* talk in absolutes :) – Marc Gravell Jul 08 '20 at 08:29
  • 1
    Thanks Peter for the link! I asked because in a video titled [Tip 5: Async libraries APIs should be chunky](https://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Async-libraries-APIs-should-be-chunky), Lucian Wischik claimed that .NET Framework preallocates tasks for the values 0 and 1. He says so at around 8:00. It is a relatively old video (2014), so things may have changed in between. The book you are referring is newer. :-) – Theodor Zoulias Jul 08 '20 at 08:59