2

Say I have following like call stack of async methods:

async Task<T> Method1()
{
   return await Method2();
}

async Task<T> Method2()
{
   return await Method3();
}

async Task<T> Method3()
{
   return await httpClient.ReadAsync(...);
}

Since only Method3 has real Async IO request. Can I just await once in Method1 as bellows? And will below approach improve the performance a bit?

async Task<T> Method1()
{
   var task = Method2();
   return await task;
}

Task<T> Method2()
{
   return Method3();
}

Task<T> Method3()
{
   return httpClient.ReadAsync(...);
}
Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
Youxu
  • 1,050
  • 1
  • 9
  • 34
  • Did you [try it out](https://github.com/dotnet/BenchmarkDotNet)? – mason Feb 24 '20 at 19:09
  • Trying it out will not be the best solution here. This is a case for google - this was EXTENSIVELY discussed in blogs by the dotnet developers including for C#8 the optimization of supporting ValueTask in order to make this faster. Trying it out will not give you those discussions. Watch https://channel9.msdn.com/Shows/On-NET/Understanding-how-to-use-Task-and-ValueTask – TomTom Feb 24 '20 at 19:17
  • @TomTom: That's unrelated, returning ValueTask and awaiting in each layer is a different matter and in the above example ValueTask changes nothing, there still needs a task to be allocated (`httpClient.ReadAsync(...)` will always return a Task and it will basically always be async and never sync).ValueTask only saves you an allocation of `Task` when it can be completed synchronously – Tseng Feb 24 '20 at 22:40
  • @TomTom What do you mean trying it out won't be the best solution? What kind of advice is that? Benchmarking the code to see how fast it runs and how much memory is allocated, and testing it to verify it returns the correct result is *exactly* what should be done. – mason Feb 25 '20 at 14:17
  • Benchmarking is particularly brutally hard, especially for micro code like that. Also benchmarking it will ignore the fact that another approach may be better - one you do not see in the benchmark. – TomTom Feb 25 '20 at 14:21
  • Relevant: [eliding async and await](https://blog.stephencleary.com/2016/12/eliding-async-await.html) – Stephen Cleary Feb 26 '20 at 14:57

1 Answers1

-2

Yes, it does, but (assuming your example) because it is REALLY bad code.

Method 1 and Method 2 should NOT be async

They should look like this:

Task Method1 (){ return Method2; }

You can do other non async stuff before Method2.

Chaining them like you do means a lot of additional tasks created and this has an impact- one reason the language ALSO accepts ValueTask now. If you do stuff in a method before the call to the submethod and all you do then is await, there is no reason to await - you can return the inner task and thus skip a complete created object.

Reusing tasks like that is in the documentatation, including i.e. pregenerating return tasks for often happening results.

If yo uare intereted, have a look at https://channel9.msdn.com/Shows/On-NET/Understanding-how-to-use-Task-and-ValueTask - those talk about performance based tasks. Sorry, video, no transcript. And then watch https://channel9.msdn.com/Events/Build/BUILD2011/TOOL-829T which talks about best practices for performance in async.

TomTom
  • 61,059
  • 10
  • 88
  • 148
  • 1
    Well, `REALLY bad code` is bit too strongly said, at least in my opinion, as while `ValueTask` is a better choice from performance point of view 1. using plain old `Task` is not that bad in most real cases 2. the question mostly asked about effects of [eliding async-await](https://blog.stephencleary.com/2016/12/eliding-async-await.html) and such question is equally relevant for both `ValueTask` and `Task` – Eugene Podskal Feb 24 '20 at 20:26
  • 1
    Gotta downvote this, since its plain wrong. As explained in the comments above, ValueTask brings no additional benefit. In this case its a) avoiding the creation (and little overhead) of creating an async state machine (async/await makes the compiler generate a statemachine to handle the async call but still look like "sync" code) b) it changes exception behavior. Awaiting a code will inspect the awaiting task and throw the exception at the point where you await,so you could catch it with try/catch. In the `return SomeMethod()` code you can't catch exceptiosn there they will he thrown on await – Tseng Feb 24 '20 at 22:44
  • Which means way further up in the code where `Method1` is awaited. This may or may not be desired and depends on the use case: A `try { return Method3(); } catch (Exception ex) { ... }` will never catch the exception, which the linked questions correctly explain too – Tseng Feb 24 '20 at 22:46