Thomas Carlton
Thomas Carlton

Reputation: 5958

How to make a loader in a separate thread?

I have a main form wich is expected to perfom some long operations. In parallel, I'm trying to display the percentage of the executed actions.

So I created a second form like this:

Private Delegate Sub DoubleFunction(ByVal D as Double)
Private Delegate Sub EmptyFunction()

Public Class LoaderClass
    Inherits Form

    'Some properties here

    Public Sub DisplayPercentage(Value as Double)
        If Me.InvokeRequired then
            dim TempFunction as New DoubleFunction(addressof DisplayPercentage)
            Me.Invoke(TempFunction, Value)
        Else
            Me.PercentageLabel.text = Value
        End if 
    End sub

    Public Sub CloseForm()
        If Me.InvokeRequired Then
            Dim CloseFunction As New EmptyFunction(AddressOf CloseForm)
            Me.Invoke(CloseFunction)
        Else
            Me.Close()
        End If

        FormClosed = True
    End Sub
End class

My main sub, the one which is expected to perform the long operations is in another form as follows:

Private Sub InitApplication
    Dim Loader as new LoaderClass
    Dim LoaderThread as new thread(Sub()
                                    Loader.ShowDialog()
                                    End sub)

    LoaderThread.start()

    Loader.DisplayPercentage(1/10)
    LoadLocalConfiguration()

    Loader.DisplayPercentage(2/10)
    ConnectToDataBase()

    Loader.DisplayPercentage(3/10)
    LoadInterfaceObjects()

    Loader.DisplayPercentage(4/10)
    LoadClients()

    ...

    Loader.CloseForm()
End sub 

The code works almost 95% of the time but sometimes I'm getting a thread exception somewhere in the sub DisplayPercentage. I change absolutely nothing, I just hit the start button again and the debugger continues the execution without any problem.

The exception I get is: Cross-thread operation not valid: Control 'LoaderClass' accessed from a thread other than the thread it was created on event though I'm using : if InvokeRequired

Does anyone know what is wrong with that code please ?

Thank you.

Upvotes: 1

Views: 788

Answers (3)

Idle_Mind
Idle_Mind

Reputation: 39122

Thank you for your proposal. How to do that please ? Where should I add Invoke ?

Assuming you've opted to leave the "loading" code of the main form in the main UI thread (probably called from the Load() event), AND you've set LoaderClass() as the "Splash screen" in Project --> Properties...

Here is what LoaderClass() would look like:

Public Class LoaderClass

    Private Delegate Sub DoubleFunction(ByVal D As Double)

    Public Sub DisplayPercentage(Value As Double)
        If Me.InvokeRequired Then
            Dim TempFunction As New DoubleFunction(AddressOf DisplayPercentage)
            Me.Invoke(TempFunction, Value)
        Else
            Me.PercentageLabel.text = Value
        End If
    End Sub

End Class

*This is the same as what you had but I moved the delegate into the class.

*Note that you do NOT need the CloseForm() method as the framework will automatically close your splash screen once the main form is completely loaded.

Now, over in the main form, you can grab the displayed instance of the splash screen with My.Application.SplashScreen and cast it back to LoaderClass(). Then simply call your DisplayPercentage() method at the appropriate times with appropriate values:

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        InitApplication()
    End Sub

    Private Sub InitApplication()
        Dim Loader As LoaderClass = DirectCast(My.Application.SplashScreen, LoaderClass)

        Loader.DisplayPercentage(1 / 10)
        LoadLocalConfiguration()

        Loader.DisplayPercentage(2 / 10)
        ConnectToDataBase()

        Loader.DisplayPercentage(3 / 10)
        LoadInterfaceObjects()

        Loader.DisplayPercentage(4 / 10)
        LoadClients()

        ' Loader.CloseForm() <-- This is no longer needed..."Loader" will be closed automatically!
    End Sub

    Private Sub LoadLocalConfiguration()
        System.Threading.Thread.Sleep(1000) ' simulated "work"
    End Sub

    Private Sub ConnectToDataBase()
        System.Threading.Thread.Sleep(1000) ' simulated "work"
    End Sub

    Private Sub LoadInterfaceObjects()
        System.Threading.Thread.Sleep(1000) ' simulated "work"
    End Sub

    Private Sub LoadClients()
        System.Threading.Thread.Sleep(1000) ' simulated "work"
    End Sub

End Class

If all goes well, your splash screen should automatically display, update with progress, then automatically close when your main form has finished loading and displayed itself.

Upvotes: 1

user2942249
user2942249

Reputation: 145

Me.Invoke(TempFunction, Value)

Should be:

Me.Invoke(TempFunction, new Object(){Value})

because the overload with parameters takes an array of parameters.

Value is on the stack of the function in the current thread. You need to allocate memory on the GC heap and copy the value to that memory so that it is available to the other thread even after the local stack has been destroyed.

Upvotes: 0

Hans Passant
Hans Passant

Reputation: 941217

This is a standard threading bug, called a "race condition". The fundamental problem with your code is that the InvokeRequired property can only be accurate after the native window for the dialog is created. The problem is that you don't wait for that. The thread you started needs time to create the dialog. It blows up when InvokeRequired still returns false but a fraction of a second later the window is created and Invoke() now objects loudly against being called on a worker thread.

This requires interlocking, you must use an AutoResetEvent. Call its Set() method in the Load event handler for the dialog. Call its WaitOne() method in InitApplication().

This is not the only problem with this code. Your dialog also doesn't have a Z-order relationship with the rest of the windows in your app. Non-zero odds that it will show behind another window.

And an especially nasty kind of problem caused by the SystemEvents class. Which needs to fire events on the UI thread. It doesn't know what thread is the UI thread, it guesses that the first one that subscribes an event is that UI thread. That turns out very poorly if that's your dialog when it uses, say, a ProgressBar. Which uses SystemEvents to know when to repaint itself. Your program will crash and burn long after the dialog is closed when one of the SystemEvents now is raised on the wrong thread.

Scared you enough? Don't do it. Only display UI on the UI thread, only execute slow non-UI code on worker threads.

Upvotes: 2

Related Questions