none
Rundown of Async / Await Pattern multi-thread issues RRS feed

  • Question

  • As we all know debugging multi-threaded applications is bear.  It just sucks.  However, I am very versed in manually handling threads, even devised my own thread pool that was more intelligent than the default .Net Thread Pool manager.  This was several years ago, so the systems may have improved since then (likely).

    I've also used the BackgroundWorker quite extensively, and as times change, languages evolve, i've started working primarily in the Task<> based async/await pattern.  But there is a problem. 

    In order to "await' within a method, that method must be declared "async".  However, when these methods get daisy-chained, so to speak, there is a lot - and i mean massive - amount of slippage. 

    For example, using the HttpClient system, the HttpContent has ReadAsStringAsync() which returns a Task<string>.

    My method awaits said call, and thus must be defined 'async'.  But the method calling it thus needs to await that method, and also must be called "async", and suddenly the UI gets to a point before any of these methods complete. 

    All the documentation says, that in a method when you "await" an async method, it allows the UI thread to continue.  This may have been updated, but it should really read the "parent thread" to continue.  When the async operation is complete it returns to the "await" line, and executes lines after that.  However, if inside the "async" method you are awaiting another method, this nesting effect causes the the first async to complete and continue before the nested one is done.

    I have tried many ways around this, but executing a Task<> based method without the "await" command, always, and i mean ALWAYS, deadlocks my program.  the Task<string>.Result never returns from it and the program hangs.  So when i have a situation of an async method that needs to be awaited, how do I get the parent async method to await as well?

    protected override async void OnLoad(object sender, EventArgs e) 
    {
        this.Items = await this.LoadItems();
        this.EndUpdate();
    }
    
    private async Task<List<ItemObject>> LoadItems() 
    {
        using (HttpClient client = new HttpClient())
        {
            url = Whatever.CreateRequest();
            HttpResponseMessage message = await client.GetAsync(url);
            string response = await message.Content.ReadAsStringAsync();
            return new List<ItemObject>(Whatever.ParseResponse<ItemObject>(response));
        }
    }
    
    private void EndUpdate() 
    {
        comboBox1.DataSource = this.Items;
        ItemObject item = comboBox1.SelectedItem as ItemObject;
        // item is ALWAYS NULL HERE because Items.Count == 0, they aren't loaded yet
    }

    in that paltry example, you see that the LoadItem returns a Task<ItemObject>  but inside that method it also await two calls to the HttpClient and HttpContent classes.  the moment the code executions hits the nested await, the await in OnLoad() is resolved, and EndUpdate executes before the data is loaded.

    Trying to not use the await, i get a Task back instead of the inner result type that the black-box of the Task system overlooks with awaiting.  When I access Task.Result the program locks. 

    How do i fix this?  What is the sequence for executing async methods so that no matter where the await lies, execution DOES NOT CONTINUE in that method until that full path of "await" calls is resolved?

    Thanks

    Jaeden "Sifo Dyas" al'Raec Ruiner

    (It seems that ever "await" call spawns a new thread.  but since i can't retrieve Task data without the "await" i'm spawning 2,3, even 4 threads for a single operation.  sometimes, i want to control when the thread is created, and all executions within that thread should be synchronous to that thread.  so if "await" spawns a thread, i need to be able to access Task Results from within the first async operation.)


    "Never Trust a computer. Your brain is smarter than any micro-chip."
    PS - Don't mark answers on other people's questions. There are such things as Vacations and Holidays which may reduce timely activity, and until the person asking the question can test your answer, it is not correct just because you think it is. Marking it correct for them often stops other people from even reading the question and possibly providing the real "correct" answer.




    Friday, May 31, 2019 10:52 PM

All replies

  • I suspect (but I'm only glancing at the code you have) that this might be caused by a SynchronizationContext problem.

    I think that the thread that handles the async completion callback (which is buried under the hood when you use async/await) is not always going to be the UI thread, in fact it can be any thread that the OS wants to assign (I think IO completion ports are involved).

    Scroll down to the section "Avoiding Context" here:

    https://blog.stephencleary.com/2012/02/async-and-await.html

    See if that helps, I think you can "force" the completion callback to run on the UI thread (the initiating thread) by using this.

    Also add some breakpoints that have an action that shows Thread.CurrentThread.ManagedThreadID then you can see which thread ID is running at each point.

    If its always the same ID over and over at every step then I'm wrong!

    Saturday, June 1, 2019 2:19 PM
  • Hi JaedenRuiner,

    Thank you for posting here.

    Based on your description, you want to run the onload method to fill the combox by using async method.

    I have modified the code, it works well.

     public class ItemObject
            {
                public string Name { get; set; }
            }
            List<ItemObject> Items = new List<ItemObject>();
            protected override async void OnLoad( EventArgs e)
            {
                this.Items = await this.LoadItems();
                this.EndUpdate();
    
            }
            private async Task<List<ItemObject>> LoadItems()
            {
                List<ItemObject> test1 = new List<ItemObject>();
                using (HttpClient client = new HttpClient())
                {
                    //url = Whatever.CreateRequest();
                    string url = "https://www.google.com/";
                    HttpResponseMessage message = await client.GetAsync(url);
                    string response = await message.Content.ReadAsStringAsync();
                    test1.Add(new ItemObject { Name = response });
                    return test1;
                }
            }
            private void EndUpdate()
            {
                comboBox1.DataSource = this.Items;
                ItemObject item = comboBox1.SelectedItem as ItemObject;
            }

    Result:

    Besides, I don't know what whatever keyword mean, so I used the other way to test it. If you want to use whatever, please provide the related code.

    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.

    Monday, June 3, 2019 6:49 AM
    Moderator
  • I took your code and (other than replacing the HttpClient with simple calls to tasks that just stall for a while) it behaves properly. There are some important things to be aware of though when talking about tasks.

    1. Using async on a void returning function (e.g. OnLoad) is a boundary case only designed to work around situations where you're making an event handler async. The underlying call is not going to be async so any code that runs after it will still run. In your case that doesn't seem to be an issue but be aware that it does change the general async nature of code. The OnLoad completes before your task is finished running. If you were calling this from some other method and that method relied on the task completing then it would fail. This is the nature of mixing void with async.

    public class Form2 : Form1
    {
       protected override void OnLoad ( EventArgs e )
       {
          //calling Form1.OnLoad which is marked async
          base.OnLoad(e);
    
          //Even though it is async it doesn't return a Task
          //so there is nothing to wait on, this code potentially
          //runs before the task in OnLoad finishes
          DoSomethingWithItems();
       }
    }

    2.  By default await will return to the same thread that originally started it. This is required for UI threads if you need to update the UI but it will deadlock if you're not careful. In almost all cases you should be using `ConfigureAwait(false)` to have the await return to an arbitrary thread. This prevents deadlock. The only time you wouldn't do that is if you wanted to update the UI. In your case this would only be needed in your OnLoad method. In all other cases use the awaiter. 

    private async Task<List<ItemObject>> LoadItems ()
    {
        var message = await DoWorkAsync().ConfigureAwait(false);
        var response = await DoWork2Async().ConfigureAwait(false);
    
        return response;
    }

    3. Await does not "spawn" new threads. The default task scheduler uses the thread pool which already has threads spun up and waiting. So you're not spinning up 3 or 4 threads but instead your code is bouncing between 3 or 4 threads to do its work. This is a key component for how functional programming works and is one of the reason why it is the way we write code these days to allow for work to more easily move around.

    4. Await/async is just a wrapper around the earlier TPL code that you can still write. So the task that gets returned from an async method is actually a task that won't complete until its continuations are done. That is how the TPL works. So having 1 or more awaits in async methods doesn't change the behavior of the returned task. It won't complete until they all complete (or an error occurs).


    Michael Taylor http://www.michaeltaylorp3.net

    Monday, June 3, 2019 3:01 PM
    Moderator