locked
Why should async programming be used throughout the entire call stack? RRS feed

  • Question

  • After doing extensive reading on the await statement, the best practice seems to be that every time an async method is called, it should be awaited.  If you don't do that, you get a warning from the compiler as shown below:

    

    The green squiggle under MyTest2() says:  "Because this call is not awaited, execution of the current method continues before the call is completed.  Consider applying the 'await' operator to the result of the call."

    To avoid the compiler warning, I need to change MyTest1() into an async method and await the call to MyTest2() as shown below:

      

    My question is, why do I have to do this?  I can see the need for this if I have code that in MyTest1() that must execute after "some MyTest2 code" in MyTest2 completes as shown above.  The alternative is the unrecommended approach whereby we wait on the Task returned by the await statement of MyTest2():


    Blocking on an await task can cause deadlock issues as well as more complicated error handling as explained in this link.

    However, if I don't need to execute any code in MyTest1() after "some MyTest2 code" finishes, then there appears to be no need to await MyTest2().  Is there some other need that I am missing? 

    Maybe the answer that someday I may add code to MyTest1() that executes after "MyTest2 code" finishes and this makes the code more maintainable.  Does anyone have any thoughts on this?  And does anyone have a practical example of when I would use multi-level async programming?

    Thanks for any help you can provide.





    • Edited by joeprog2 Thursday, September 27, 2018 5:09 AM
    Thursday, September 27, 2018 4:53 AM

Answers

  • If you don't want to post process the task then just return the Task instead of returning void..

    e.g.

    private Task MyTest1()

    {

     return MyTest2();

    }

    This informs the caller of "MyTest1" that there is a task that needs to be awaited, rather than awaiting it within MyTest1.

    NB.

    Generally, once you introduce async code you should go "async all the way" and at some point somebody has to await the completion of the task - for example ASP.NET controllers can return a Task<ActionResult> in which case the framework takes care of the final await.

    Maybe you're talking about fire & forget which is whole other topic depending on your environment, again in ASP.NET you can't simply fire & forget because the thread will get killed before completion - in this case have a look at QueueBackgroundWorkItem.

    If your not talking about fire & forget and for some reason you really must switch from async back to synchronous in your code, then yes it will have to block using .Result or .Wait() (or some other approach to avoid deadlocks) then that's what you have to do. Generally speaking if the code correctly uses ConfigureAwait(false) down the call stack then you wont get deadlock issues when blocking on it.

    --

    I see from your linked article that you have already come across some content by Stephen Cleary - he is a great resource for async programming paradigms and caveats.

    I have tried to summarise and exemplify some of the learnings on my blog post here:

    http://www.craigwardman.com/Blogging/BlogEntry/async-all-the-way-conventions

    • Edited by craigwardman Thursday, September 27, 2018 11:15 AM added link to further material
    • Marked as answer by joeprog2 Sunday, September 30, 2018 5:54 AM
    Thursday, September 27, 2018 10:57 AM

All replies

  • If you don't want to post process the task then just return the Task instead of returning void..

    e.g.

    private Task MyTest1()

    {

     return MyTest2();

    }

    This informs the caller of "MyTest1" that there is a task that needs to be awaited, rather than awaiting it within MyTest1.

    NB.

    Generally, once you introduce async code you should go "async all the way" and at some point somebody has to await the completion of the task - for example ASP.NET controllers can return a Task<ActionResult> in which case the framework takes care of the final await.

    Maybe you're talking about fire & forget which is whole other topic depending on your environment, again in ASP.NET you can't simply fire & forget because the thread will get killed before completion - in this case have a look at QueueBackgroundWorkItem.

    If your not talking about fire & forget and for some reason you really must switch from async back to synchronous in your code, then yes it will have to block using .Result or .Wait() (or some other approach to avoid deadlocks) then that's what you have to do. Generally speaking if the code correctly uses ConfigureAwait(false) down the call stack then you wont get deadlock issues when blocking on it.

    --

    I see from your linked article that you have already come across some content by Stephen Cleary - he is a great resource for async programming paradigms and caveats.

    I have tried to summarise and exemplify some of the learnings on my blog post here:

    http://www.craigwardman.com/Blogging/BlogEntry/async-all-the-way-conventions

    • Edited by craigwardman Thursday, September 27, 2018 11:15 AM added link to further material
    • Marked as answer by joeprog2 Sunday, September 30, 2018 5:54 AM
    Thursday, September 27, 2018 10:57 AM
  • Thanks for your quick and informative response.  Let me re-phrase my question another way:

    If I ignore the green squiggle (compiler warning mentioned above) and implement the code as shown, how will this hurt me?

    Friday, September 28, 2018 1:34 AM
  • It depends on where the code is being run, in a windows/console app I imagine the code will still execute in the background (like fire & forget) unless the user closes the app first. So in your example something somewhere will be doing a delay.

    In an ASP.NET environment the code could be torn down at any point after the request ends, so it cannot really be determined what will have happened by then in the asynchronous call, for example the thread running the delay could be destroyed before the time has elapsed.


    Friday, September 28, 2018 7:45 AM
  • I understand now why for ASP.NET we need to do await all the way up the call stack.  It also makes sense for Console programs because if we did not get an await in the main() method, we could exit main() and exit the app without finishing what we are awaiting on.

    In the case of desktop programs such as WPF or Winforms, we could probably get away with not awaiting all the way up the call stack if we were handling an event.  However, await at each level would make it easy to add logic that we want to execute only when all lower level awaits are complete.

    I am going to close this incident now and give credit for the answer to craigwardman.  Thank you.

    By the way, I also have another "await" question in the UWP forum at this link if anybody wants to take a look.

    Sunday, September 30, 2018 5:54 AM
  • Another factor is that if we don't await an async method, it causes problems with exception handling as explained here
    Sunday, April 26, 2020 4:36 AM