Nostromo
Nostromo

Reputation: 1274

Why do my tasks don't run parallel?

I put together a little test program to learn about multi threading in VB.net. After a lot of researching, trying and failing I got my program running, at least a bit.

Now it behaves oddly and I can't explain why. Maybe you can tell me, what's wrong with my program, why it behaves the way it does and/or what I have to change to get it working the way it's supposed to work.

My program contains a class (TestClass), that simulates the long running tasks that I want to each execute on a different thread, a progress form (Form1) to show progress messages from my long running tasks and the main procedure, which puts these two together.

Here's my code:

Progress form (Form1) containing two listboxes (ListBox1 and ListBox2):

Public Class Form1

Public Sub AddMessage1(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Me.ListBox1.Items.Add(message)
  Me.ListBox1.SelectedIndex = Me.ListBox1.Items.Count - 1
  Application.DoEvents()
End Sub

Public Sub AddMessage2(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Me.ListBox2.Items.Add(message)
  Me.ListBox2.SelectedIndex = Me.ListBox2.Items.Count - 1
  Application.DoEvents()
End Sub

End Class

TestClass:

Imports System.Threading

Public Class TestClass

  Public Event ShowProgress(ByVal message As String)

  Private _milliSeconds As UShort
  Private _guid As String

  Public Sub New(milliSeconds As UShort, ByVal guid As String)
    _milliSeconds = milliSeconds
    _guid = guid
  End Sub

  Public Function Run() As UShort
    For i As Integer = 1 To 20
      RaiseEvent ShowProgress("Run " & i)
      Thread.Sleep(_milliSeconds)
    Next i

    Return _milliSeconds
  End Function

End Class

Main procedure:

Imports System.Threading.Tasks
Imports System.ComponentModel

Public Class Start
  Private Const MULTI_THREAD As Boolean = True

  Public Shared Sub Main()
    Dim testClass(1) As TestClass
    Dim testTask(1) As Task(Of UShort)
    Dim result(1) As UShort

    testClass(0) = New TestClass(50, "Test1")
    testClass(1) = New TestClass(200, "Test2")

    Using frm As Form1 = New Form1
      frm.Show()

      AddHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      AddHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      If MULTI_THREAD Then
        testTask(0) = Task(Of UShort).Factory.StartNew(Function() testClass(0).Run, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext)
        testTask(1) = Task(Of UShort).Factory.StartNew(Function() testClass(1).Run, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext)

        Task.WaitAll(testTask)

        result(0) = testTask(0).Result
        result(1) = testTask(1).Result
      Else
        result(0) = testClass(0).Run
        result(1) = testClass(1).Run
      End If

      RemoveHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      RemoveHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      frm.Close()
    End Using

    MessageBox.Show("Result 1: " & result(0) & "; Result 2: " & result(1))
  End Sub

End Class

This program is supposed to open the progress form, run the two long running tasks in parallel, show the progress messages of the two long running tasks, close the progress form when both long running tasks are finished and show a message box with the results of the long running tasks.

Now to the behaviour I can't explain: If I run this program, it shows a "Run 1" in both listboxes, so both tasks started running, but then only the first listbox gets filled and only when the first listbox is filled (the first task completed), the second listbox continues getting filled. So, both tasks get started, but the second one waits for the first one to finish before continuing running.
But, when I comment out the Task.WaitAll(testTask) in my main procedure, it's the other way around. It shows a "Run 1" in both listboxes, then the second listbox gets filled and only when the second listbox is filled (the second task completed), the first one continues running.

Why does my program behaves so oddly and how can I make it run my two tasks at the same time?

Thank you for your help in solving this little mystery :-)


Update
Since the only answer so far stated that it has to do with the SynchronizationContext I'm using I removed it from the code of my main procedure:

Main procedure:

Imports System.Threading.Tasks
Imports System.ComponentModel

Public Class Start
  Private Const MULTI_THREAD As Boolean = True

  Public Shared Sub Main()
    Dim testClass(1) As TestClass
    Dim testTask(1) As Task(Of UShort)
    Dim result(1) As UShort

    testClass(0) = New TestClass(50, "Test1")
    testClass(1) = New TestClass(200, "Test2")

    Using frm As Form1 = New Form1
      frm.Show()

      AddHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      AddHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      If MULTI_THREAD Then
        testTask(0) = Task(Of UShort).Factory.StartNew(Function() testClass(0).Run)
        testTask(1) = Task(Of UShort).Factory.StartNew(Function() testClass(1).Run)

        Task.WaitAll(testTask)

        result(0) = testTask(0).Result
        result(1) = testTask(1).Result
      Else
        result(0) = testClass(0).Run
        result(1) = testClass(1).Run
      End If

      RemoveHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      RemoveHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      frm.Close()
    End Using

    MessageBox.Show("Result 1: " & result(0) & "; Result 2: " & result(1))
  End Sub

End Class

To avoid an InvalidOperationException because of illegal cross-thread calls, I changed the code of my form as follows:

Progress form (Form1) containing two listboxes (ListBox1 and ListBox2):

Public Delegate Sub ShowProgressDelegate(ByVal message As String)

Public Class Form1

Public Sub AddMessage1(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Debug.Print("out List 1: " & message)

  If Me.InvokeRequired Then
    Me.BeginInvoke(New ShowProgressDelegate(AddressOf Me.AddMessage1), message)
  Else
    Debug.Print("in List 1: " & message)

    Me.ListBox1.Items.Add(message)
    Me.ListBox1.SelectedIndex = Me.ListBox1.Items.Count - 1
    Application.DoEvents()
  End If
End Sub

Public Sub AddMessage2(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Debug.Print("out List 2: " & message)

  If Me.InvokeRequired Then
    Me.BeginInvoke(New ShowProgressDelegate(AddressOf Me.AddMessage2), message)
  Else
    Debug.Print("in List 2: " & message)

    Me.ListBox2.Items.Add(message)
    Me.ListBox2.SelectedIndex = Me.ListBox2.Items.Count - 1
    Application.DoEvents()
  End If
End Sub

End Class

I just added a few Debug.Prints for debugging and the InvokeRequired parts.

Now both tasks run parallel (I know because of the Debug.Print calls with the "out List" messages) but my progress messages don't show in the listboxes, the according code never gets executed (the Debug.Print calls with the "in List" messages don't get shown in the debug window).

I googled for the right way to do the InvokeRequired part and the way I did it seems to be right, but it doesn't work.
Can someone please tell me, what's wrong?

Thank you again for your help.

Upvotes: 1

Views: 406

Answers (2)

Nostromo
Nostromo

Reputation: 1274

After a lot of searching and trying I came up with a solution. It's not pretty but it works.

It seems to me, that Task.WaitAll() not only waits for the handed over tasks to finish but also blocks the task it's called on.
So, changing the code of my main procedure from

...
Task.WaitAll(testTask)
...

to

...
Do While Not Task.WaitAll(testTask, 100)
  Application.DoEvents
Loop
...

does the trick and my progress messages get shown in the listboxes.

Upvotes: 0

Damien_The_Unbeliever
Damien_The_Unbeliever

Reputation: 239824

You're telling the system to run the tasks on the current synchronization context.

TaskScheduler.FromCurrentSynchronizationContext

In this instance, that synchronization context is the UI thread - of which there's only one. So the tasks will queue up until they can run on the UI thread - which only becomes available when you enter the WaitAll() call (because the rules of WaitAll say that, if the current thread is suitable for running one or more of the tasks (it is) and those same tasks haven't yet started (which they can't have), the current thread may directly execute one or more of those tasks)

You could ask for the tasks to be run on a different synchronization context (or the thread pool), but then you encounter a different issue - in the events that that threads are raising, you're interacting with UI elements.

Maybe you should look into the BackgroundWorker class, which runs code on a separate thread, but is specifically built for reporting progress back to the UI/Foreground thread.

Upvotes: 2

Related Questions