如何在单独的线程中创建一个加载器?

时间:2013-11-18 21:11:45

标签: vb.net multithreading

我有一个主要形式,预计将执行一些长期操作。与此同时,我试图显示已执行操作的百分比。

所以我创建了第二个这样的形式:

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

我的主要子,预期执行长操作的那个子的另一种形式如下:

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 

代码的工作时间几乎 95%但有时我会在sub DisplayPercentage 中的某处获得线程异常。我什么都没改,我只是再次点击开始按钮,调试器继续执行没有任何问题。

我得到的异常是:跨线程操作无效:控制'LoaderClass'从一个线程以外的线程访问事件虽然我正在使用:如果InvokeRequired

有人知道该代码有什么问题吗?

谢谢。

3 个答案:

答案 0 :(得分:2)

这是一个标准的线程错误,称为“竞争条件”。您的代码的基本问题是,在创建对话框的本机窗口之后,InvokeRequired属性只能是准确的。问题是你不要等待。您启动的线程需要时间来创建对话框。当InvokeRequired仍然返回 false 时它会爆炸但是稍后会创建一个窗口,而Invoke()现在会大声反对在工作线程上调用。

这需要互锁,您必须使用AutoResetEvent。在对话框的Load事件处理程序中调用其Set()方法。在InitApplication()中调用它的WaitOne()方法。

这是此代码的唯一问题。您的对话框与应用程序中的其余窗口也没有Z顺序关系。非零赔率,它会在后面显示另一个窗口。

由SystemEvents类引起的一种特别讨厌的问题。哪个需要在UI线程上触发事件。它不知道UI线程是什么线程,它猜测订阅事件的第一个是UI线程。如果这是你的对话,当它使用ProgressBar时,结果非常糟糕。它使用SystemEvents知道何时重绘自己。当现在在错误的线程上引发其中一个SystemEvents时,关闭对话框后,程序将崩溃并长时间刻录。

吓到你了吗?不要这样做。仅在UI线程上显示UI,仅在工作线程上执行缓慢的非UI代码。

答案 1 :(得分:1)

  

感谢您提出的建议。请问怎么做?我应该在哪里   添加Invoke?

假设您已选择在主UI线程中保留主窗体的“加载”代码(可能从Load()事件中调用), AND 您已设置LoaderClass()作为Project中的“启动画面” - >属性...

这是LoaderClass()的样子:

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

*这与你所拥有的相同,但我将代表移到了班级。

*请注意,您不需要CloseForm()方法,因为一旦主窗体完全加载,框架将自动关闭启动屏幕。

现在,在主窗体中,您可以使用My.Application.SplashScreen获取闪屏的显示实例,并将其强制转换为LoaderClass()。然后在适当的时间使用适当的值调用DisplayPercentage()方法:

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

如果一切顺利,您的初始屏幕应自动显示,随进度更新,然后在主表单加载完成并自行显示后自动关闭。

答案 2 :(得分:0)

Me.Invoke(TempFunction, Value)

应该是:

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

因为带参数的重载需要一系列参数。

值在当前线程中的函数堆栈上。您需要在GC堆上分配内存并将值复制到该内存,以便即使在本地堆栈被销毁后,其他线程也可以使用它。