0

I have a large initialization routine within a VB.NET Framework 4.0 application, that I want to optimize for run time. As most computers nowadays are capable of performing multiple threads at a time, I want to introduce multi-threading to perform non-dependent in parallel, so the overall time to the user it takes for initializing the application is reduced.

The original (single thread) implementation looks like this:

Friend Module MainModule

    Friend Sub Main()
        LongRunningInitialization1()
        LongRunningInitialization2()
        LongRunningInitialization3()
        LongRunningInitialization4()
        LongRunningInitialization5()
        LongRunningInitialization6()
        LongRunningInitialization7()
    End Sub

    Private Sub LongRunningInitialization1()
    End Sub

    Private Sub LongRunningInitialization2()
    End Sub

    Private Sub LongRunningInitialization3()
    End Sub

    Private Sub LongRunningInitialization4()
    End Sub

    Private Sub LongRunningInitialization5()
    End Sub

    Private Sub LongRunningInitialization6()
    End Sub

    Private Sub LongRunningInitialization7()
    End Sub

End Module

My first approach was it to use System.Threading.Thread to parallelize the workloads:

Friend Sub Main()
    Dim thread1 As New Threading.Thread(AddressOf LongRunningInitialization1)
    Dim thread2 As New Threading.Thread(AddressOf LongRunningInitialization2)
    Dim thread3 As New Threading.Thread(AddressOf LongRunningInitialization3)
    Dim thread4 As New Threading.Thread(AddressOf LongRunningInitialization4)
    Dim thread5 As New Threading.Thread(AddressOf LongRunningInitialization5)
    Dim thread6 As New Threading.Thread(AddressOf LongRunningInitialization6)
    Dim thread7 As New Threading.Thread(AddressOf LongRunningInitialization7)
    thread1.Join()
    thread2.Join()
    thread3.Join()
    thread4.Join()
    thread5.Join()
    thread6.Join()
    thread7.Join()
End Sub

While this rudimentary works, Join() is a blocking call and in a screnario in which thread1 takes the longest time, the other threads are "zombies" as long as thread1 has finished. Also a not here implemented splash screen with a progress bar freezes as Join() is a blocking call.

I came up with another "solution" by using a while and theThreadState` property of a thread to do non-blocking wait in a hacky way:

Friend Sub Main()
    Dim thread1 As New Threading.Thread(AddressOf LongRunningInitialization1)
    Dim thread2 As New Threading.Thread(AddressOf LongRunningInitialization2)
    Dim thread3 As New Threading.Thread(AddressOf LongRunningInitialization3)
    Dim thread4 As New Threading.Thread(AddressOf LongRunningInitialization4)
    Dim thread5 As New Threading.Thread(AddressOf LongRunningInitialization5)
    Dim thread6 As New Threading.Thread(AddressOf LongRunningInitialization6)
    Dim thread7 As New Threading.Thread(AddressOf LongRunningInitialization7)
    While thread1.ThreadState = Threading.ThreadState.Running _
        OrElse thread2.ThreadState = Threading.ThreadState.Running _
        OrElse thread3.ThreadState = Threading.ThreadState.Running _
        OrElse thread4.ThreadState = Threading.ThreadState.Running _
        OrElse thread5.ThreadState = Threading.ThreadState.Running _
        OrElse thread6.ThreadState = Threading.ThreadState.Running _
        OrElse thread7.ThreadState = Threading.ThreadState.Running
        Application.DoEvents() ' process the Windows Forms message queue.
        Threading.Thread.Sleep(1) ' non-blocking wait.
    End While
    thread1.Join()
    thread2.Join()
    thread3.Join()
    thread4.Join()
    thread5.Join()
    thread6.Join()
    thread7.Join()
End Sub

While this last implementation works in this example, it is not usable when you face the real world with around 20 long running initialization tasks that should mostly run in parallel. The while condition would be epic in size.

Is there a solution to perform a non-blocking wait for all the threads which where created? Something like the following pseudo code:

Friend Sub Main()
    Dim threadPool As New PseudoThreadPool()
    threadPool.Add(New Threading.Thread(AddressOf LongRunningInitialization1))
    threadPool.Add(New Threading.Thread(AddressOf LongRunningInitialization2))
    threadPool.Add(New Threading.Thread(AddressOf LongRunningInitialization3))
    threadPool.Add(New Threading.Thread(AddressOf LongRunningInitialization4))
    threadPool.Add(New Threading.Thread(AddressOf LongRunningInitialization5))
    threadPool.Add(New Threading.Thread(AddressOf LongRunningInitialization6))
    threadPool.Add(New Threading.Thread(AddressOf LongRunningInitialization7))
    threadPool.Start()
    While threadPool.AllDone = False
        Application.DoEvents() ' process the Windows Forms message queue.
        Threading.Thread.Sleep(1) ' non-blocking wait.
    End While
    threadPool.Join()
End Sub
burnersk
  • 3,320
  • 4
  • 33
  • 56
  • 1
    Can't you use a `List(Of Task)` and `await Task.WhenAll([Your List(Of Task)])`? – Jimi Feb 08 '19 at 08:41
  • @Jimi : That function is not available with .NET Framework 4.0. I also have updated my question because of the version. – burnersk Feb 08 '19 at 09:00
  • What's the blocker to upgrading to a supported version of .NET? – Damien_The_Unbeliever Feb 08 '19 at 09:08
  • @Damien_The_Unbeliever : Windows XP is not supported anymore (4.0.3 is the last version with XP compatibility) – burnersk Feb 08 '19 at 09:18
  • [TaskFactory.ContinueWhenAll](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskfactory.continuewhenall) -- [Parallel.ForEach](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreach) – Jimi Feb 08 '19 at 09:21
  • That DoEvents() call is a risk, make sure you guard all onClosing code against that. In testing, close your Form while this is still running. – H H Feb 08 '19 at 09:46
  • @HenkHolterman I am opening and closing/dispose several forms (some 3rd party initializations causes forms to be shown) while in the `DoEvents` `while` loop. I never experienced any issue. Can you elaborate your risk? Or did you miss that I run `DoEvents` within a module and not within a form? – burnersk Feb 08 '19 at 09:49
  • @HenkHolterman : Are you referring to [this](https://stackoverflow.com/a/5183623/1016105)? Because my startup "form" is not a form but a `Main` sub in a module. So there is no "main form" that causes issues. The `while` is looping until the application is ready with the startup. After that the user is presented with a (call it so) "main form". Even when the user manages to close the splash screen (which is mostly prevented), the "main form" will be shown once the initialization is done. – burnersk Feb 08 '19 at 10:09

1 Answers1

1

I have found a solution, thanks to @Jimi (List(Of Task) approach).

Friend Sub Main()
    Dim tasks As New List(Of Task)
    tasks.Add(New Task(AddressOf LongRunningInitialization1))
    tasks.Add(New Task(AddressOf LongRunningInitialization2))
    tasks.Add(New Task(AddressOf LongRunningInitialization3))
    tasks.Add(New Task(AddressOf LongRunningInitialization4))
    tasks.Add(New Task(AddressOf LongRunningInitialization5))
    tasks.Add(New Task(AddressOf LongRunningInitialization6))
    tasks.Add(New Task(AddressOf LongRunningInitialization7))
    ' Start all tasks.
    tasks.All(
        Function(t As Task)
            t.Start()
            Return True
        End Function
    )
    ' Wait until all tasks has been finished.
    While tasks.Any(Function(t As Task) Not (t.Status = TaskStatus.Canceled OrElse t.Status = TaskStatus.Faulted OrElse t.Status = TaskStatus.RanToCompletion))
        Application.DoEvents()
        Sleep(1)
    End While
End Sub
burnersk
  • 3,320
  • 4
  • 33
  • 56