none
Trying to understand Task.Wait(). Not doing what I thought it would do. RRS feed

  • Question

  • I've tried to read up and understand .NET Task.Wait().  As I understand it, Wait tells the calling thread to stop execution until the task is done executing... 

    RunningTwoWithWait() gets called by the UI thread (button click) in a Windows Form app.  Once t1.Wait(); is called, my GUI freezes up. As you can see, UpdateText() is not doing very much. Just writing text to a text box 500 times, which executes pretty quick.  

    All I wanted to test out was trying to force T2 to wait to execute until after T1.  I did achieve this affect with calling ContinueWith, but I was hoping achieve it by waiting.

    It seems as though I definitely don't fully grasp what Wait is doing.  Was hoping someone could explain to me why this is doing what I was hoping for. 

            private void RunningTwoWithWait()
            {
                var t1 = Task.Factory.StartNew (UpdateText, "ONE");
                t1.Wait( );
                var t2 = Task.Factory.StartNew (UpdateText, "TWO");
            }

            private void UpdateText( object state )
            {
                for( int i = 0; i < 500; i++ )
                {
                    richTextBox1.Invoke( new Action( ( ) => richTextBox1.Text += "\n" + state + " " + i ) );
                }
            }

    Thanks for any help!!!

    Rick


    Rick

    Friday, January 18, 2019 8:31 PM

Answers

  • You are correct that Wait causes the calling thread to block waiting for the task to complete. However where things fall down in your example is with UpdateText. Let's break it down by the (logical) threads this code is running on.

    UI thread - RunningTwoWithWait is called
    UI thread - New task started for t1
    T1 thread - Calls Invoke to update the UI
    UI thread - Action from Invoke is executed
    T1 thread - Calls Invoke to update the UI
    UI thread - Action from Invoke is executed
    ...
    UI thread - Wait is called which freezes UI thread
    T1 thread - Calls Invoke to update the UI 
    Deadlock

    The UI requires that all updates occur on the UI thread. To make that happen you are (correctly) calling Invoke. Invoke pushes a message onto the UI thread's message queue and waits for it to be processed. That message (and therefore the action you specify) is run on the UI thread. This is what allows you to update the UI.

    However at some point after you started t1 you block the UI thread with the Wait. Because the UI is now blocked waiting for the T1 task to complete and the task is blocked waiting for the UI to process its message request you have deadlocked the system. This is a very special situation caused by trying to get two threads to sync to the same thread (the UI) and is a common source of issues.

    In your very specific case you said you wanted to run t2 after t1 so there are a couple of solutions that prevent the deadlocking. The first option, sticking with the raw usage of Task is to use the ContinueWith. 

    private void RunningTwoWithWait ()
    {
        var t1 = Task.Run(() => UpdateText("ONE"))
                        .ContinueWith(t => UpdateText("TWO"));
    }
    
    private void UpdateText ( string message )
    {
        //Using Console here but your for loop would work as well
        Console.WriteLine(message);
    }

    In general you shouldn't call Wait (or any other blocking call) in a UI thread so in this case it is left out. This has the side effect that RunningTwoWithWait will actually return to its caller before the tasks finish running. Normally this is a bad idea but it is permitted in a few places such as responding to events in the UI. If you really wanted to wait then you'd need to continually check the status of the thread and process messages at the same time otherwise you'd deadlock.

    The preferred approach is to use async/await. This is how you'd write it if this method were called from an event in your UI.

    private async void RunningTwoWithWait ()
    {
        //Called from UI thread
        await UpdateTextAsync("ONE");
    
        //Called from UI thread as well
        await UpdateTextAsync("TWO");
    }
    
    private async Task UpdateTextAsync ( string message )
    {
        //Using Console here but your for loop would work as well
        Console.WriteLine(message);
    }
    This is effectively equivalent to the previous code but notice that you can more easily understand the flow and you're not "waiting" anymore. The method will still return before the task(s) complete.


    Michael Taylor http://www.michaeltaylorp3.net

    Friday, January 18, 2019 9:12 PM
    Moderator

All replies

  • You are correct that Wait causes the calling thread to block waiting for the task to complete. However where things fall down in your example is with UpdateText. Let's break it down by the (logical) threads this code is running on.

    UI thread - RunningTwoWithWait is called
    UI thread - New task started for t1
    T1 thread - Calls Invoke to update the UI
    UI thread - Action from Invoke is executed
    T1 thread - Calls Invoke to update the UI
    UI thread - Action from Invoke is executed
    ...
    UI thread - Wait is called which freezes UI thread
    T1 thread - Calls Invoke to update the UI 
    Deadlock

    The UI requires that all updates occur on the UI thread. To make that happen you are (correctly) calling Invoke. Invoke pushes a message onto the UI thread's message queue and waits for it to be processed. That message (and therefore the action you specify) is run on the UI thread. This is what allows you to update the UI.

    However at some point after you started t1 you block the UI thread with the Wait. Because the UI is now blocked waiting for the T1 task to complete and the task is blocked waiting for the UI to process its message request you have deadlocked the system. This is a very special situation caused by trying to get two threads to sync to the same thread (the UI) and is a common source of issues.

    In your very specific case you said you wanted to run t2 after t1 so there are a couple of solutions that prevent the deadlocking. The first option, sticking with the raw usage of Task is to use the ContinueWith. 

    private void RunningTwoWithWait ()
    {
        var t1 = Task.Run(() => UpdateText("ONE"))
                        .ContinueWith(t => UpdateText("TWO"));
    }
    
    private void UpdateText ( string message )
    {
        //Using Console here but your for loop would work as well
        Console.WriteLine(message);
    }

    In general you shouldn't call Wait (or any other blocking call) in a UI thread so in this case it is left out. This has the side effect that RunningTwoWithWait will actually return to its caller before the tasks finish running. Normally this is a bad idea but it is permitted in a few places such as responding to events in the UI. If you really wanted to wait then you'd need to continually check the status of the thread and process messages at the same time otherwise you'd deadlock.

    The preferred approach is to use async/await. This is how you'd write it if this method were called from an event in your UI.

    private async void RunningTwoWithWait ()
    {
        //Called from UI thread
        await UpdateTextAsync("ONE");
    
        //Called from UI thread as well
        await UpdateTextAsync("TWO");
    }
    
    private async Task UpdateTextAsync ( string message )
    {
        //Using Console here but your for loop would work as well
        Console.WriteLine(message);
    }
    This is effectively equivalent to the previous code but notice that you can more easily understand the flow and you're not "waiting" anymore. The method will still return before the task(s) complete.


    Michael Taylor http://www.michaeltaylorp3.net

    Friday, January 18, 2019 9:12 PM
    Moderator
  • Hi Michael...

    Thanks for the reply, I greatly appreciate it. It's still early enough in the morning that the coffee has not fully kicked in, but I think I understand. At least to some degree why it caused problems. 

    The main UI thread couldn't update anymore because of the wait() call. Therefore when I was trying to update the GUI in the T1 thread, in the Invoke action, the GUI was stuck waiting. 

    Yes?  Did I get that right? :)


    Rick

    Sunday, January 20, 2019 6:34 PM
  • "Yes?  Did I get that right? :)"

    Yes because the Invoke call needs to run on main UI thread which is currently waiting.


    Michael Taylor http://www.michaeltaylorp3.net

    Sunday, January 20, 2019 8:45 PM
    Moderator