locked
Need help understanding what is happening behind the scenes in async/await RRS feed

  • Question

  • I recently wrote some code that used async/await and it deadlocked which served to show me that I didn't understand async/await technology well enough. Can someone please explain to me what's happening under the covers when a separate Task begins asynchronous operations, what happens to the caller when it hits the await, why the thread can continue to execute even though the thread is paused at the await and what happens when the asynchronous code completes and satisfies the await condition.

    Here's an example of code that, according to the source, would cause a deadlock. Most explanations I've seen for why this deadlock happens is that the context is not available so the called method cannot complete and the caller can't release the context because it is waiting for the called method to complete. This sounds like something that would be said by someone who understands deadlocks but doesn't understand the underlying mechanism is in this case. There have be a few instances that give a slightly different explanation, that the async operation at the end of the chain needs to post an event that would be processed by the thread's message pump and would let the caller know that the async has completed but the thread's message pump has been blocked because further up the chain an async function was called synchronously. The idea of a non-foreground non-UI thread having a message pump is a concept that I'd not been exposed to before and I'm not sure that I'm understanding things correctly. So without further ado, here's the sample bad code. Let's discuss what is happening and why it deadlocks.

    public void Button_Click()
    {
        var task = GetTextAsync("/path/to/file.txt");
        
        // Dead lock here.
        var result = task.Result;
    
        this.SomeTextControl.Text = result;
    }
    
    
    public static async Task<string> GetTextAsync(string filePath)
    {
        using (var stream = System.IO.File.OpenRead(filePath))
        {
            using (var reader = new System.IO.StreamReader(stream))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }


    Richard Lewis Haggard

    Thursday, September 12, 2019 9:42 PM

Answers

  • Here's a simplified explanation of the mechanics behind the deadlock.

    The 'task.Result' line is a blocking operation. Its thread is blocked until Result is available. Since this call is being made as a result of a user action it is operating on the foreground thread but, as said, it is blocked. That's the first part.

    Now, to the other half of this deadlock, the 'await reader.ReadToEndAsync()'. When the ReadToEndAsync is called a callback is generated for use later when the task completes. So, the call is made, a thread goes off to do the work and the foreground immediately returns to its caller. The caller hits the task.Result and is blocked until the Result is available to be released. The ReadToEndAsync continues to run in the background and at some point completes. The callback is intended to be used to handle completion notification but, because it was constructed on a foreground thread, can't execute because the foreground thread is locked up waiting for task.Result to be available which won't happen because the callback can't run. Deadlock forever.

    See? That wasn't so hard.



    Richard Lewis Haggard

    Monday, September 16, 2019 3:52 PM

All replies

  • There are no easy answers to understanding what is happening behind the scenes in async/await. If there were then someone would have written an article with it. It is unlikely anyone can provide an easier answer here than what already exists in articles and such.

    So your question should be limited to the example you have provided but do not expect the answer to be enlightening enough to answer the general question.



    Sam Hobbs
    SimpleSamples.Info

    Friday, September 13, 2019 1:14 AM
  • I don't totally understand async/await either but I am nearly certain that async/await do not create tasks, that the task must be created by something else. In your example the ReadToEndAsync will create a task. But why are you doing that? You are trying to completely read the file and put the text in SomeTextControl before the click handler finishes. So why are you trying to be asynchronous? It seems you can do it all synchronously. If you want to do it asynchronously then create a task to do the read and then exit the click handler and then when the data is available, put it in the control.

    If however this is just a sample that is intended to begin a discussion of async/await then it won't likely work. You are not likely to get a clear answer. You can make your post a discussion.



    Sam Hobbs
    SimpleSamples.Info



    • Edited by Simple Samples Friday, September 13, 2019 1:38 AM spelling
    Friday, September 13, 2019 1:36 AM
  • Hi Richard,

    Thank you for posting here.

    As Simple said, understanding what happened behind the scenes in async/await is not a easy thing. I suggest that you could read more articles about it.

    Also, I find the following link may be helpful for you.

    Async and Await

    Note:This response contains a reference to a third party World Wide Web site. Microsoft is providing this information as a convenience to you. Microsoft does not control these sites and has not tested any software or information found on these sites; Therefore, Microsoft cannot make any representations regarding the quality, safety, or suitability of any software or information found there. There are inherent dangers in the use of any software found on the Internet, and Microsoft cautions you to make sure that you completely understand the risk before retrieving any software from the Internet.

    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.

    Friday, September 13, 2019 1:58 AM
  • Try setting up the button click as public async void Button_Click() then call GetTextAysnc as

    var result = await GetTextAsync().ConfigureAwait(false);

    From there you will not be able to directly set the TextBox Text but instead but use this.SomeTextControl.Invoke (see the following for a guide).

    Can not guarantee this will solve this issue but worth a try.


    Please remember to mark the replies as answers if they help and unmarked them if they provide no help, this will help others who are looking for solutions to the same or similar problem. Contact via my Twitter (Karen Payne) or Facebook (Karen Payne) via my MSDN profile but will not answer coding question on either.

    NuGet BaseConnectionLibrary for database connections.

    StackOverFlow
    profile for Karen Payne on Stack Exchange

    Friday, September 13, 2019 2:01 AM
  • The point of the question was to acquire understanding of what was happening in the given scenario, not to collect alternatives. As much as I appreciate such response please understand that the intent is to understand the process, not fix a purposely incorrect example.

    Richard Lewis Haggard

    Friday, September 13, 2019 2:51 PM
  • Does this sound about right? Disregarding the Task.Run/spawning of threads and concentrating on the async/await part of this, I think that this is a fairly accurate without getting bogged down into the details.

    There’s a lot more to it than this but a simplistic explanation of how the async/await pattern works goes like this:

    • A block of code has the async attribute associated with it
    • An await is encountered
    • At the point at which the await resides the thread’s current context is saved and halted
    • Current code jumps to the next instruct past the point where the 'async' method was called.
    • {This part about 'message pumps' is questionable. May be immaterial. May be just plain wrong.} The thread’s message pump is in a fit state to continue operations.
    • The message pump continues to process and dispatch incoming messages as they arrive.
    • When the await condition is resolved a message is dropped onto the thread's message queue.
    • When this message is processed by the pump it tells the thread that it needs to reassert the saved context.
    • The thread resumes execution from the point in code that the await paused.

    Notice that this says nothing about Task/thread spawn and such. I haven't quite gotten through what it is about original example's task.Result that is contributing to the blocked message pump. I still don't understand what is actually happening under the covers there well enough to explain things in an accurate but dramatically over simplified fashion.


    Richard Lewis Haggard


    • Edited by Richard.Haggard Friday, September 13, 2019 7:13 PM New data acquired that showed previous states to be inaccurate
    Friday, September 13, 2019 3:19 PM
  • Umm, no. Don't take anything that I said in the previous post as gospel. The more I read and learn the more I think my own understanding is seriously flawed but do not yet know enough to feel confident about writing up another high level explanation.

    Richard Lewis Haggard

    Monday, September 16, 2019 1:08 PM
  • Here's a simplified explanation of the mechanics behind the deadlock.

    The 'task.Result' line is a blocking operation. Its thread is blocked until Result is available. Since this call is being made as a result of a user action it is operating on the foreground thread but, as said, it is blocked. That's the first part.

    Now, to the other half of this deadlock, the 'await reader.ReadToEndAsync()'. When the ReadToEndAsync is called a callback is generated for use later when the task completes. So, the call is made, a thread goes off to do the work and the foreground immediately returns to its caller. The caller hits the task.Result and is blocked until the Result is available to be released. The ReadToEndAsync continues to run in the background and at some point completes. The callback is intended to be used to handle completion notification but, because it was constructed on a foreground thread, can't execute because the foreground thread is locked up waiting for task.Result to be available which won't happen because the callback can't run. Deadlock forever.

    See? That wasn't so hard.



    Richard Lewis Haggard

    Monday, September 16, 2019 3:52 PM
  • Wow! Good sleuthing, Richard!  =0)

    ~~Bonnie DeWitt [C# MVP]

    http://geek-goddess-bonnie.blogspot.com

    Wednesday, September 18, 2019 4:55 AM