10

I'm working on some code which builds a buffer in memory and then empties it into a TextWriter when the buffer fills up. Most of the time, the character will go straight into the buffer (synchronously) but occasionally (once every 4kb) I need to call TextWriter.WriteAsync.

In the System.Threading.Tasks.Extensions package there only appears to be a ValueTask<T> struct, and no non-generic ValueTask (without a type parameter). Why is there no ValueTask, and what should I do if I need to convert a method returning a non-generic Task (that is, the async equivalent of a void method) to ValueTask?

Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • What behavior would you expect from a `ValueTask`? By definition a `ValueTask` can be used when the result is available synchronously. If there's no result what's the point or benefit over returning `Task`? – JSteward Feb 22 '18 at 19:30
  • @JSteward I'm working on something which builds a buffer in memory and then empties it into a `TextWriter` when the buffer fills up. Most of the time, the character will go straight into the buffer (synchronously) but occasionally (once every 4kb) I need to call `TextWriter.WriteAsync`. – Benjamin Hodgson Feb 22 '18 at 19:31
  • I'm not sure how you'd benefit from `ValueTask` but the docs suggest that _For uses other than consuming the result... ValueTask can lead to a more convoluted programming model_. Maybe you could redesign your solution using something like [TPL-Dataflow](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/dataflow-task-parallel-library) that has numerous ways to buffer and process data. – JSteward Feb 22 '18 at 19:35
  • 2
    @JSteward Thanks for the suggestion, and you weren't to know this, but Dataflow is massive overkill for my purposes, much as I love it. I want this code to be fast and not allocate, and I am willing to use a convoluted programming model to achieve that :) – Benjamin Hodgson Feb 22 '18 at 19:38

2 Answers2

5

Shot in the dark, but I think it's because Task.CompletedTask is sufficient for most non-generic cases.

One way to think of ValueTask<T> is as a union of Task<T> and T (for asynchronous and synchronous cases respectively). Accordingly a non-generic ValueTask would be a union of Task and... nothing, so just a Task.

I can't think of a case where a non-generic ValueTask would be practically different than caching an already completed Task (which is what Task.CompletedTask is), though I'd love to learn about any.

Kevin Montrose
  • 22,191
  • 9
  • 88
  • 137
  • I don’t quite agree that a non-generic `ValueTask` would be exactly the same as “just a task”. More like `Optional` - the wrapped `Task` would be null in the case that the operation completed synchronously. You can see why this might be if you set `T` to some `struct NoReturnValue {}` in the formula `ValueTask ~ T | Task`. I _may_ be able to return `Task.CompletedTask` in my case, I’ll give it a go when I get home – Benjamin Hodgson Feb 22 '18 at 20:12
  • It's not clear to me what the logical distinction between a `Task` where `IsCompleted == true` and a null value is (obviously, in practice the latter throws). In both cases you know not to suspend execution, and the generated state machine will check `IsCompleted` before doing so. – Kevin Montrose Feb 22 '18 at 20:21
  • Yeah it looks like this is going to work, thanks for the help Kevin. I was worried that an `async` method might always build a new `Task`, even if it completed synchronously, but from reading `AsyncTaskMethodBuilder` it looks like that's not the case; an async `Task`-returning method returns a cached completed `Task` if it completed synchronously. – Benjamin Hodgson Feb 22 '18 at 20:56
  • I do think it was a rather strange API design decision to not include a non-generic `ValueTask`. It's not at all obvious what you're supposed to do. – Benjamin Hodgson Feb 22 '18 at 20:57
  • 1
    [Relevant new proposal](https://github.com/dotnet/corefx/issues/27445) that _would_ add a non-generic `ValueTask`. It becomes necessary if you want to re-use the allocation required in the asynchronous case. – Kevin Montrose Feb 25 '18 at 17:53
2

Based on this article, you can change any asynchronous method directly from Task to ValueTask, but you must beware of its usage patterns mentioned in the article; specifically:

  • Awaiting the instance multiple times.
  • Calling AsTask multiple times.
  • Using more than one of these techniques to consume the instance.

And if you need to get the result back as a Task / Task<TResult>, you should use .AsTask().

katrash
  • 1,065
  • 12
  • 13