none
Async and Wait RRS feed

  • Question

  • A little background. I did programming 20 years ago. I wrote DLLs which mediated the data from a classic ASP page into a database and returned data back to the user. So coding knowledge of new practices is self taught so could be wrong.

    A year ago I came back to programming as my new job required it. The requirement was to create a web API that takes post requests containing XML documents, and posts them into a Sage system. The XML is deserialised and I take out what I need, create a new object, and post it in as a JSON request through Sage's web API. The details of the work done in the controller to deal with the XML and output the JSON aren't important. What's important is that under load my web API has trouble. I think it is probably my code is not written well as I've been somewhat self taught in this endeavour in learning how to perform post requests, and living in a multi-threaded world. I developed the API in .NET Core 2.1.

    My controller function definition looks like this:

    public string Post([FromBody] bDataEntity bData, [FromQuery] string s = "")


    In this function I have this call:

    ProcessBFile(APIURI + sageCompanyName + "/", bData).Wait();


    From what I've been reading, this is bad practice because in the ProcessBFile function this is ultimately where it pulls the data from the bData XML and creates the JSON data that it will post into Sage, there are some external processes that get called. So towards the end of ProcessBFile, after the data is pushed into Sage, I call an external process to do some additional work. The call is done like this:

    await RunPostReadinessAndPost(...);


    It seems I am mixing .Wait() with await and from what I've been reading, that is not good. "Async all the way down" is what I've been reading. So, does that mean in the Post handler in the controller, I should change it to:

    public async Task<string> Post([FromBody] bDataEntity bData, [FromQuery] string s = "")
    {
    .
    .
    .
    await ProcessBFile(APIURI + sageCompanyName + "/", bData);
    


    In the RunPostReadinessAndPost function, I start a new Process() as it launches an external exe file which I need to wait for to get the exit code as this forms part of my response to the client in the request/response cycle.

    I think over the last year in doing this, although it works, it is probably not well written, but when you're on your own, up against the clock you search for answers that work (or seem to) and don't research whether it is the right way or best way to do the job. Any thoughts or input?

    Another query is what happens when a second post request comes in before the other one has completed? If, during the part where it calls an external process. will it just create another running instances of that called executable?
    • Edited by MrSnert Tuesday, April 2, 2019 11:53 AM General edit
    Tuesday, April 2, 2019 11:38 AM

Answers

  • Firstly, let's separate concerns. Questions related to web API and ASP.NET should be posted in the ASP.NET forums. So we'll skip over that aspect.

    Yes, async all the way. So your entry point (controller action in this case) should be async. ASP.NET already supports this so you simply make the action async and return a Task. You should also accept a CancellationToken as the last argument so it can be cancelled. 

    Once you are inside an async method you then make calls to other async methods. The general rule of thumb is that if the method takes any reasonable amount of time or relies on other external sources then it should be async as well. Database calls, for example, async. A function that takes your model and makes a business object out of it however can be sync. 

    Let's talk about what happens when you actually use await and this will direct the rest of the conversation. Imagine this function.

    public void CallFoo ()
    {
       FooAsync().Wait();
    }
    
    public async Task FooAsync ()
    {
       Bar1();
    
       await Bar2Async();
    
       Bar3();
    }

    `FooAsync` gets (logically) rewritten to this.

    public Task FooAsync ()
    {
       Bar1();
    
       var task = Task.Run(() => Bar2Async())
    
       task.ContinueWith(() => Bar3());
    
       return task;
    }

    Await breaks up your function. Everything before the first await executes in the method as normal. The awaited function returns a task and this is what gets returned back to the caller. At this point your FooAsync returns and the caller continues on. Notice the threading here. Everything up to the await runs on the thread that called the method (your action's thread in your case).

    Because you had more work after the await the compiler adds a continuation to the task that executes the remaining code in your method. If you had an another await later then it'll just keep adding continuations. When the first task completes it "continues with" the next block until ultimately everything is done at which point the task completes.

    If you call `Wait` on a task it waits until the original task and all continuations are done before it returns, hence your thread blocks. For a web API action that returns Task the runtime is already set up to handle this so it'll wait until your action "completes" before returning a response to the remote client. You would not call Wait in this case as that defeats the purpose.

    Going back to that continuation. Your task will be run on a thread pool thread. When it completes the runtime needs to know where to continue the execution. Remember the existing thread (that originally called your code) has already moved on so you have to decide. In most cases you don't care where the remaining work runs. This is where `ConfigureAwait(false)` comes in. Some people argue that it should have been the default but it isn't. Putting this at the end of a Task returning call tells the runtime that you don't care what thread executes the continuation. In almost all cases this is the behavior and hence what you'll do. 

    The only cases where you want to ensure the code continues executing on the original thread is for Windows apps that are doing UI updates (which have to happen on the UI thread) or for code that is thread sensitive (some COM calls or anything that relies on TLS). But these are exceptions rather than the norm. This type of code is more likely to deadlock though so you have to be careful. There are plenty of articles discussing how to do this.

    Back to web API. The runtime allocates a request thread from its pool to handle your request. Since your action returns a Task at some point it'll wait or otherwise return the task. The runtime then puts your task into a "wait" queue until it is done. Meanwhile it releases the request thread back to the pool. This allows the runtime to handle a lot of requests without blocking waiting on tasks. When your task completes the runtime needs to get a request thread again. However it isn't going to go back to your original request thread because web apps don't work that way so you'll get an arbitrary one. Hence for a web app using ConfigureAwait(false) is correct because the runtime is going to behave that way anyway. Could you leave it off and get the same behavior? Yes, but everybody puts it there so it is clear. 

    Summary of how it would work.

    public async Task<string> SomeApi ( CancellationToken cancellationToken )
    {
       return await FooAsync(cancellationToken).ConfigureAwait(false);
    }
    
    private async Task FooAsync ( CancellationToken cancellationToken )
    {
       var items = await GetDataFromDBAsync(cancellationToken).ConfigureAwait(false);
    
       //This is a sync method, so make it async
       await Task.Run(() => ProcessData(items).ConfigureAwait(false);
    
       await SaveDataAsync(items, cancellationToken).ConfigureAwait(false);
    }
    
    //A regular sync method
    private void ProcessData ( ... )
    {
    }


    Michael Taylor http://www.michaeltaylorp3.net

    Tuesday, April 2, 2019 2:15 PM
    Moderator

All replies

  • Firstly, let's separate concerns. Questions related to web API and ASP.NET should be posted in the ASP.NET forums. So we'll skip over that aspect.

    Yes, async all the way. So your entry point (controller action in this case) should be async. ASP.NET already supports this so you simply make the action async and return a Task. You should also accept a CancellationToken as the last argument so it can be cancelled. 

    Once you are inside an async method you then make calls to other async methods. The general rule of thumb is that if the method takes any reasonable amount of time or relies on other external sources then it should be async as well. Database calls, for example, async. A function that takes your model and makes a business object out of it however can be sync. 

    Let's talk about what happens when you actually use await and this will direct the rest of the conversation. Imagine this function.

    public void CallFoo ()
    {
       FooAsync().Wait();
    }
    
    public async Task FooAsync ()
    {
       Bar1();
    
       await Bar2Async();
    
       Bar3();
    }

    `FooAsync` gets (logically) rewritten to this.

    public Task FooAsync ()
    {
       Bar1();
    
       var task = Task.Run(() => Bar2Async())
    
       task.ContinueWith(() => Bar3());
    
       return task;
    }

    Await breaks up your function. Everything before the first await executes in the method as normal. The awaited function returns a task and this is what gets returned back to the caller. At this point your FooAsync returns and the caller continues on. Notice the threading here. Everything up to the await runs on the thread that called the method (your action's thread in your case).

    Because you had more work after the await the compiler adds a continuation to the task that executes the remaining code in your method. If you had an another await later then it'll just keep adding continuations. When the first task completes it "continues with" the next block until ultimately everything is done at which point the task completes.

    If you call `Wait` on a task it waits until the original task and all continuations are done before it returns, hence your thread blocks. For a web API action that returns Task the runtime is already set up to handle this so it'll wait until your action "completes" before returning a response to the remote client. You would not call Wait in this case as that defeats the purpose.

    Going back to that continuation. Your task will be run on a thread pool thread. When it completes the runtime needs to know where to continue the execution. Remember the existing thread (that originally called your code) has already moved on so you have to decide. In most cases you don't care where the remaining work runs. This is where `ConfigureAwait(false)` comes in. Some people argue that it should have been the default but it isn't. Putting this at the end of a Task returning call tells the runtime that you don't care what thread executes the continuation. In almost all cases this is the behavior and hence what you'll do. 

    The only cases where you want to ensure the code continues executing on the original thread is for Windows apps that are doing UI updates (which have to happen on the UI thread) or for code that is thread sensitive (some COM calls or anything that relies on TLS). But these are exceptions rather than the norm. This type of code is more likely to deadlock though so you have to be careful. There are plenty of articles discussing how to do this.

    Back to web API. The runtime allocates a request thread from its pool to handle your request. Since your action returns a Task at some point it'll wait or otherwise return the task. The runtime then puts your task into a "wait" queue until it is done. Meanwhile it releases the request thread back to the pool. This allows the runtime to handle a lot of requests without blocking waiting on tasks. When your task completes the runtime needs to get a request thread again. However it isn't going to go back to your original request thread because web apps don't work that way so you'll get an arbitrary one. Hence for a web app using ConfigureAwait(false) is correct because the runtime is going to behave that way anyway. Could you leave it off and get the same behavior? Yes, but everybody puts it there so it is clear. 

    Summary of how it would work.

    public async Task<string> SomeApi ( CancellationToken cancellationToken )
    {
       return await FooAsync(cancellationToken).ConfigureAwait(false);
    }
    
    private async Task FooAsync ( CancellationToken cancellationToken )
    {
       var items = await GetDataFromDBAsync(cancellationToken).ConfigureAwait(false);
    
       //This is a sync method, so make it async
       await Task.Run(() => ProcessData(items).ConfigureAwait(false);
    
       await SaveDataAsync(items, cancellationToken).ConfigureAwait(false);
    }
    
    //A regular sync method
    private void ProcessData ( ... )
    {
    }


    Michael Taylor http://www.michaeltaylorp3.net

    Tuesday, April 2, 2019 2:15 PM
    Moderator
  • `FooAsync` gets (logically) rewritten to this.

    Your explanation is very useful. Is there Microsoft documentation explaining how async methods use tasks internally?



    Sam Hobbs
    SimpleSamples.Info

    Tuesday, April 2, 2019 6:24 PM
  • MSDN blogs had a good writeup back when it was introduced.

    Michael Taylor http://www.michaeltaylorp3.net

    Tuesday, April 2, 2019 7:07 PM
    Moderator
  • Hi

    Is your problem solved? If so, please post "Mark as answer" to the appropriate answer, so that it will help other members to find the solution quickly if they face a similar issue.

    Best Regards,

    Jack


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Tuesday, April 9, 2019 3:00 AM
    Moderator
  • That is a great explanation. I'll see what I can do with it now :)
    Wednesday, April 10, 2019 10:34 AM