Reputation: 1274
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
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
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