none
Task.Factory.StartNew - 1st Task Disappears! RRS feed

  • Question

  • I have this very simple code (below) which uses Task.Factory.StartNew.  

    Option Strict On
    Option Infer Off
    Option Explicit On
    
    Imports System.Threading
    
    
    Public Class Form1
    
        Private Sub btnDoIt_Click(sender As Object, e As EventArgs) Handles btnDoIt.Click
    
            Dim uiContext As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
    
            ' Start a task - this runs on the background thread...
            Dim worktask As task = Task.Factory.StartNew(Sub()
                                                             Dim i As Integer
                                                             For i = 1 To 10
    
                                                                 Thread.Sleep(1500)
    
                                                                 Task.Factory.StartNew(Sub()
                                                                                           TextBox1.Text = "Delay " + i.ToString() + " has completed"
                                                                                       End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    
                                                             Next i
    
                                                         End Sub)
    
        End Sub 'btnDoIt_Click
    
    End Class


    (Sorry about the way that code is formatted but it is Visual Studio's fault.)  The problem is that I never see the update which should occur when i = 1!  And if I set a breakpoint on the statement which is updating TextBox1.Text the first time the debugger stops is when i has a value of 2!  If I change the For statement to "For i = 1 To 1" then the one and only debugger stop on the updating statement is when, again, i has a value of 2!  When I let the 1 To 10 loop run to completion the final text in TextBox1 is "Delay 11 has completed".  

    If I run this code ...

    ublic Class Form1
    
        Private Sub btnDoIt_Click(sender As Object, e As EventArgs) Handles btnDoIt.Click
    
            Dim uiContext As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
    
    
    
            Dim i As Integer
            For i = 1 To 10
    
                Thread.Sleep(1500)
    
                TextBox1.Text = "Delay " + i.ToString() + " has completed"
                Me.Update()
    
            Next i
    
            ' Start a task - this runs on the background thread...
            Dim worktask As task = Task.Factory.StartNew(Sub()
                                                             Dim ii As Integer
                                                             For ii = 1 To 10
    
                                                                 Thread.Sleep(1500)
    
                                                                 Task.Factory.StartNew(Sub()
                                                                                           TextBox1.Text = "Delay " + ii.ToString() + " has completed"
                                                                                       End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    
                                                             Next ii
    
                                                         End Sub)
    
        End Sub 'btnDoIt_Click
    
    End Class


    The first loop, run on the UI thread, produces the result one would expect.  The second loop, run on the background thread, produces the 2-11 messages.

    I tried another experiment with the loop in the task unwound ...

    Dim uiContext As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
    
            ' Start a task - this runs on the background thread...
            Dim worktask As task = Task.Factory.StartNew(Sub()
    
                                                             Thread.Sleep(1500)
    
                                                             Task.Factory.StartNew(Sub()
                                                                                       TextBox1.Text = "Delay 1 has completed"
                                                                                   End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    
                                                             Thread.Sleep(1500)
    
                                                             Task.Factory.StartNew(Sub()
                                                                                       TextBox1.Text = "Delay 2 has completed"
                                                                                   End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    
                                                         End Sub)


    This produced the expected result.

    And finally, because this is such a scary bug, I tried a For Each experiment ...

    Dim uiContext As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
    
            Dim whatever As List(Of Char) = New List(Of Char)
    
            whatever.Add("A"c)
            whatever.Add("B"c)
    
            Dim worktask As task = Task.Factory.StartNew(Sub()
    
                                                             For Each c As Char In whatever
                                                                 Thread.Sleep(1500)
    
                                                                 Task.Factory.StartNew(Sub()
                                                                                           TextBox1.Text = "Delay " & c.ToString & " has completed"
                                                                                       End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    
                                                             Next c
    
                                                         End Sub)

    And that too produced the expected result.  

    Is this an outright bug or some misunderstanding on my part?

    Thanks,  Bob


    Saturday, June 14, 2014 3:07 PM

Answers

  • This has to do with the way lambda expressions work - they "capture" variables and not their values. In your example this means that the lambda expressions used to update the textbox captures the i variable. But the task won't execute immediately and the variable can and will likely change before the task runs. That's how you end up seeing 2-11 instead of 1-10.

    The fix is simple, copy the value of i to a variable that's declared inside the loop. You could even do the whole string concatenation outside the task:

                    For i = 1 To 10
                        Thread.Sleep(1500)
                        Dim text As String = "Delay " + i.ToString() + " has completed"
                        Task.Factory.StartNew(
                            Sub()
                                TextBox1.Text = text
                            End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
                    Next i
    

    • Marked as answer by eBob.com Saturday, June 14, 2014 10:26 PM
    Saturday, June 14, 2014 4:42 PM
    Moderator
  • Well, the short version first:

    It's because of the sleep in the update task. That code runs on the UI thread, do not sleep on the UI thread.

    The long version is complicated:

    Maybe you have heard that UI threads have a "message queue". A UI thread sits in a "message loop" and gets messages from it's message queue. These messages are responsible for a lot of things, from keyboard input to painting.

    Running a task on a UI thread by using the task scheduler obtained via FromCurrentSynchronizationContext also makes use of the message queue, basically a message gets posted to that queue that tells the UI thread to run the task.

    Messages are normally processed in the order they have been posted but there's a notable exception: painting messages have low priority and they are processed only when the message queue is empty.

    So, let's put all this information together and see how it all works:

    1. a message gets posted to tell the UI thread to run the update task
    2. the UI thread gets the message and executes the task
    3. the UI thread sleeps for 2 seconds - no messages can be processed while the thread sleeps
    4. the Text property is set - a bunch of code runs on the UI thread here and this code ends by invalidating the textbox control. This causes a paint message to be posted to the message queue.
    5. the update task terminates and control is returned to the message loop

    Things should pretty clear until here. The tricky part follows:

    1. the UI task likely needed more time to update the textbox, there's a lot of code involved in doing that. Let's say that the UI task took 2002ms to complete.
    2. the background task simply goes to the next iteration after it starts the update task, the time taken between the start of 2 update tasks is likely smaller than the time taken by the UI task - let's say it's 2001ms.
    3. The result of this timing affair is that when the UI thread tries to get a new message it finds a message posted to start another update task.
    4. Because of the low priority of the paint message this new "start update task" message is processed and the paint message is left in the queue - the textbox control doesn't update

    I'm not sure how clear is all this but you can make an experiment that may help understanding what's going on:

    Decrease a bit the sleep timeout in the update task, use 1900 instead of 2000 for example. You'll likely notice that the UI updates properly in this case.

    • Marked as answer by eBob.com Saturday, June 14, 2014 10:25 PM
    Saturday, June 14, 2014 8:07 PM
    Moderator

All replies

  • This has to do with the way lambda expressions work - they "capture" variables and not their values. In your example this means that the lambda expressions used to update the textbox captures the i variable. But the task won't execute immediately and the variable can and will likely change before the task runs. That's how you end up seeing 2-11 instead of 1-10.

    The fix is simple, copy the value of i to a variable that's declared inside the loop. You could even do the whole string concatenation outside the task:

                    For i = 1 To 10
                        Thread.Sleep(1500)
                        Dim text As String = "Delay " + i.ToString() + " has completed"
                        Task.Factory.StartNew(
                            Sub()
                                TextBox1.Text = text
                            End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
                    Next i
    

    • Marked as answer by eBob.com Saturday, June 14, 2014 10:26 PM
    Saturday, June 14, 2014 4:42 PM
    Moderator
  • Thank you very much Mike.  I should have figured that out.  But your solution has the same problem doesn't it?  That is, the task has only the address of the variable and when the value will be picked up is unpredictable.  In this code ...

    Dim worktask As task = Task.Factory.StartNew(Sub()
    
    Dim i As Integer
    Dim ii As Integer
    For i = 1 To 10
      ii = i
      Thread.Sleep(2000)
      Task.Factory.StartNew(Sub()
                              Thread.Sleep(2000)
                              TextBox1.Text = "Delay " + ii.ToString() + " has completed"
                            End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    Next i
    
                                                 End Sub)

    ... I only see the final (10th) message in the TextBox - although I know from using the debugger that the updating statement (TextBox1.Text = ...) is being executed 10 times.  

    And the other implication of this is that the coder is still very responsible for synchronizing/locking access to variables, right?  E.G. if I had a long string which was being assembled in a loop, instead of an integer being incremented, and then being displayed via the UI thread (e.g. TextBox1.Text = longstring) then w/o synchronizing/locking code the UI could display a garbled string.  Right?  

    Again, thanks very much.  I'll be interested in your response to my comments above.

    Bob

    Saturday, June 14, 2014 6:05 PM
  • "In this code ..."

    This code isn't like my code, you moved the additional variable, ii, outside the for loop. It has to be inside the for loop like my 'text' variable. When it is inside the loop each iteration gets a different variable so there's no interference between iterations.

    "And the other implication of this is that the coder is still very responsible for synchronizing/locking access to variables, right?"

    Well, yes. In general, if you use any sort of threading you have to take care about synchronization when accessing shared variables.

    But in this particular case there's nothing special you need to do if you move ii inside the loop. You'd only get into trouble if you modify ii after starting the textbox update task.

    Saturday, June 14, 2014 6:16 PM
    Moderator
  • Thanks for your continued help, and patience, Mike.  When I read the first part of your reply I slapped my forehead with the palm of my hand and said to myself "Idiot - you keep forgetting that Dim is executable".  So ... I fixed my code ...

    Dim worktask As task = Task.Factory.StartNew(Sub()
    
                                                             Dim i As Integer
    
                                                             For i = 1 To 10
                                                                 Dim ii As Integer
                                                                 ii = i
                                                                 Thread.Sleep(2000)
    
                                                                 Task.Factory.StartNew(Sub()
                                                                                           Thread.Sleep(2000)
                                                                                           TextBox1.Text = "Delay " + ii.ToString() + " has completed"
                                                                                       End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    
                                                             Next i
    
                                                         End Sub)

    ... and ... same visual result!  I saw only the final update message.  But I understood what you said and it made perfect sense so I tried this ...

    Dim worktask As task = Task.Factory.StartNew(Sub()
    
                                                             Dim i As Integer
    
                                                             For i = 1 To 10
                                                                 Dim ii As Integer
                                                                 ii = i
                                                                 Thread.Sleep(2000)
    
                                                                 Task.Factory.StartNew(Sub()
                                                                                           Thread.Sleep(2000)
                                                                                           TextBox1.Text = "Delay " + ii.ToString() + " has completed"
                                                                                           tbxAllUpdates.Text &= TextBox1.Text & "   "
                                                                                       End Sub, CancellationToken.None, TaskCreationOptions.None, uiContext)
    
                                                             Next i
    
                                                         End Sub)

    ... so I could not be misled by the updates taking place faster than my eye could see them, and then saw this ...

    ... confirming what you always knew and what I have finally got through my thick head.  

    But I still have one question which I hope you will have the patience to help me with.  With the code as above I see nothing on the form until the loop has completed when I see all updates at the same time.  It seems to me that I should see the first update after 4 seconds.  When I remove the Sleep inside the updating task then I see updates every two seconds - what I'd expect.  How does that Sleep inside the updating task delay all updates until the loop has finished?

    Thanks,  Bob

    Saturday, June 14, 2014 7:20 PM
  • Well, the short version first:

    It's because of the sleep in the update task. That code runs on the UI thread, do not sleep on the UI thread.

    The long version is complicated:

    Maybe you have heard that UI threads have a "message queue". A UI thread sits in a "message loop" and gets messages from it's message queue. These messages are responsible for a lot of things, from keyboard input to painting.

    Running a task on a UI thread by using the task scheduler obtained via FromCurrentSynchronizationContext also makes use of the message queue, basically a message gets posted to that queue that tells the UI thread to run the task.

    Messages are normally processed in the order they have been posted but there's a notable exception: painting messages have low priority and they are processed only when the message queue is empty.

    So, let's put all this information together and see how it all works:

    1. a message gets posted to tell the UI thread to run the update task
    2. the UI thread gets the message and executes the task
    3. the UI thread sleeps for 2 seconds - no messages can be processed while the thread sleeps
    4. the Text property is set - a bunch of code runs on the UI thread here and this code ends by invalidating the textbox control. This causes a paint message to be posted to the message queue.
    5. the update task terminates and control is returned to the message loop

    Things should pretty clear until here. The tricky part follows:

    1. the UI task likely needed more time to update the textbox, there's a lot of code involved in doing that. Let's say that the UI task took 2002ms to complete.
    2. the background task simply goes to the next iteration after it starts the update task, the time taken between the start of 2 update tasks is likely smaller than the time taken by the UI task - let's say it's 2001ms.
    3. The result of this timing affair is that when the UI thread tries to get a new message it finds a message posted to start another update task.
    4. Because of the low priority of the paint message this new "start update task" message is processed and the paint message is left in the queue - the textbox control doesn't update

    I'm not sure how clear is all this but you can make an experiment that may help understanding what's going on:

    Decrease a bit the sleep timeout in the update task, use 1900 instead of 2000 for example. You'll likely notice that the UI updates properly in this case.

    • Marked as answer by eBob.com Saturday, June 14, 2014 10:25 PM
    Saturday, June 14, 2014 8:07 PM
    Moderator
  • Thanks once more Mike.  It had occurred to me that my updating code was keeping the UI thread fully occupied.  But I hadn't realized that the paint messages had lower priority than other work.  I should have realized it because I have learned that cpu bound work often needs a Me.Update to get the UI updated - I just had never really thought about why.  

    For the benefit of anyone who stumbles across this thread ... a Me.Update in the updating task also insures that the UI gets updated.

    Thanks again Mike for your patience and thorough explanations. 

    Bob

    Saturday, June 14, 2014 10:33 PM
  • "a Me.Update in the updating task also insures that the UI gets updated"

    Yes, that would work. Update sends a paint message and because it "sends" rather than "post" it gets around the message queue.

    Anyway, you should not sleep in the UI thread. The whole point of using tasks in this case is to move work from the UI thread to a background thread so that the UI thread is free to process messages. Because of this Update should rarely be needed.

    Sunday, June 15, 2014 6:23 AM
    Moderator
  • Hi Mike,

    Yes, I get that the UI thread should not Sleep.  I was using Sleep just to simulate some CPU grinding. 

    I never thought about the fact that Update would not work if it was being handled like any other message.  I did a Google search and read a bit about send vs. post - mostly over my head but interesting.

    Thanks again for all of your help.

    Bob

    Monday, June 16, 2014 4:04 PM