0

I'm trying to understand if there's an optimisation in skipping async/await in certain situations or whether this could lead to a pitfall. Code below is made up example but it'll help to simplify for my question and understanding.

Say I have the following code:

public async Task<string> CreateRandomString()
{
    var myTitleTask = GetTitle();

    var randomString = CreateRandomString();

    var myTitle = await myTitleTask;
    
    return myTitle + randomString;
}

private async Task<string> GetTitle()
{
    return await GetTitleFromWebServiceAsync();
}

It's possible to remove the async/await from the GetTitle() method like this:

public async Task<string> CreateRandomString()
{
    var myTitleTask = GetTitle();

    var randomString = CreateRandomString();

    var myTitle = await myTitleTask;
    
    return myTitle + randomString;
}

private Task<string> GetTitle()
{
    return GetTitleFromWebServiceAsync();
}

Does this optimise anything because we are delaying as long as possible to await the Task from GetTitle() and thus doing other work until we await or does this cause any problems?

In the second example I thought this was more optimise and better approach but just want to make sure I'm not falling into any pitfall. Thoughts or comments on this please?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
millie
  • 2,642
  • 10
  • 39
  • 58
  • you should await GetTitle(), not myTitleTask – onedevteam.com Jun 14 '22 at 10:38
  • There is no problem with the second example. – shingo Jun 14 '22 at 10:39
  • @onedevteam.com - It's fine (though slightly unusual) to do it as shown above, to save a reference to the Task and then await it later. – T.J. Crowder Jun 14 '22 at 10:40
  • 6
    [Eliding Async and Await](https://blog.stephencleary.com/2016/12/eliding-async-await.html) by Stephen Cleary. – Theodor Zoulias Jun 14 '22 at 10:42
  • [Async Guidance](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#prefer-asyncawait-over-directly-returning-task) by David Fowler – Peter Csala Jun 14 '22 at 10:49
  • why do you call CreateRandomString() inside the same method? are you sure your code is even completing. it seems like its calling it self endless without returning anything – M.Hazara Jun 14 '22 at 10:58

4 Answers4

1

Does this optimise anything because we are delaying as long as possible to await the Task from GetTitle() and thus doing other work until we await or does this cause any problems?

Well, I'm not going to go into benchmarks but this is what I sometimes do. If I need to "scan" a folder for images or some files. I start tasks in a foreach loop of directories and enumerate through them to get the relevant files, exiting the loop and await them all i.e. look through each folder in parallel. Instead of Task t = Task.Run(() => foo()); we can always do as you suggested, simply return the task instead of awaiting it and then in the end await myTask or await myTasks

There are also other uses like in a microservices architecture, you might want to "fire and forget" about an order created, such that email service, notification service, etc... handle all that and you return success to the user. Pretty much just like you place an order on Amazon and you only get a thank you for placing the order. For a sequential function like yours, I don't see a benefit of awaiting it on the 3rd line or the 5th line. It really depends on the code.

AMunim
  • 992
  • 6
  • 13
  • I fully agree with you, the only stuff that should be added, is that in the first case, from example, in stack trace will be also `GetTitle` method, in second one stack trace will be without it. So as you mentioned it depends on the code, but in my experience was case when with such approach, returning `Task` was hard to investigate the issue as there was a lot of such returns from diff places. – choper Jun 14 '22 at 11:31
  • yes you are correct indeed, I remember putting breakpoints in a method that somehow jumped on the first line on a step next, I had to then comment `Tasks` and just run them normally to check where I made the mistake and put them back in :P – AMunim Jun 14 '22 at 15:49
1

I'm trying to understand if there's an optimisation in skipping async/await in certain situations or whether this could lead to a pitfall.

When you do a single call in a method it's safe to omit async+await.

Task Method1Async()
  => Method2Async(); // Super safe

When there are multiple statements in the method things can go wrong.

From Stephen Cleary's Eliding Async and Await:

One of the most common mistakes in eliding async and await is that developers forget that there is code at the end of their method that needs to run at the appropriate time. In particular, when using a using statement:

public async Task<string> GetWithKeywordsAsync(string url)
{
   using (var client = new HttpClient())
       return await client.GetStringAsync(url);
}

public Task<string> GetElidingKeywordsAsync(string url)
{
   using (var client = new HttpClient())
       return client.GetStringAsync(url);
}

In this example, eliding the keywords will abort the download.

The article also shows issues when exceptions are involved and a more nuanced case of AsyncLocal and the exceptions part is relevant to your example (because you do Task t = ... ; other code; await t;):

public async Task<string> GetWithKeywordsAsync()
{
    string url = /* Something that can throw an exception */;
    return await DownloadStringAsync(url);
}

public Task<string> GetElidingKeywordsAsync()
{
    string url = /* Something that can throw an exception */;
    return DownloadStringAsync(url);
}

(...)

var task = GetWithKeywordsAsync();
var result = await task; // Exception thrown here

var task = GetElidingKeywordsAsync(); // Exception thrown here
var result = await task;
tymtam
  • 31,798
  • 8
  • 86
  • 126
  • I'd probably expand on the exception angle. If `GetTitle` throws, the exception will be raised immediately. If `async GetTitle` throws, the exception is only raised when you `await` the result. – Jeremy Lakeman Jun 17 '22 at 01:04
  • @JeremyLakeman very good point. I'll expand – tymtam Jun 17 '22 at 01:09
0

It's possible to remove the async/await from the GetTitle() method like this:

Yes, and it is perfectly safe to do so.

Does this optimise anything because we are delaying as long as possible to await the Task from GetTitle() and thus doing other work until we await or does this cause any problems?

They are directly equivalent, skipping a async/await should be slightly more efficient, but I would expect the difference to be minimal. I would recommend doing some benchmarking if you are interested in actual numbers.

JonasH
  • 28,608
  • 2
  • 10
  • 23
-1

Yes, when you find yourself only using await in the return statement, removing the async keyword from the method signature optimizes performance and bundle size. The C# compiler turns each method marked with async to a separate class that inherits IAsyncStateMachine! It basically divides the async method into several synchronous portions(2 awaits = 3 portions) and executes each portion depending on the class state. Thus, having modified the method to a class, an overhead of 100 bytes is added to your final bundle size(usually a very minor problem nowadays). And there is also the performance overhead of the runtime having to switch threads when the await keyword is present. So returning the task without awaiting it when possible will result in less context-switching, which is quite expensive.

Jamal Salman
  • 199
  • 1
  • 10