VB.NET(WinForms)中带参数的安全线程池队列

Posted

技术标签:

【中文标题】VB.NET(WinForms)中带参数的安全线程池队列【英文标题】:Safe ThreadPool Queueing with Parameters in VB.NET (WinForms) 【发布时间】:2014-10-12 01:33:52 【问题描述】:

我知道如何使用 BackgroundWorker(WinForms 设计器中的 gui 对象),并手动实例化将自定义事件提升到 UI 的线程,但是,我在弄清楚如何使用 ThreadPool 对象(最简单的形式)时遇到了一些麻烦处理将事件提升到表单以进行“安全” UI 操作。

示例如下:

Form1.vb

    Public Class Form1
        WithEvents t As Tools = New Tools

        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            t.Unzip("file 1", "foo")
            t.Unzip("file 2", "foo")
            t.Unzip("file 3", "foo")
            t.Unzip("file 4", "foo")
            t.Unzip("file 5", "foo")
            t.Unzip("file 6", "foo")
            t.Unzip("file 7", "foo")
            t.Unzip("file 8", "foo")
            t.Unzip("file 9", "foo")

        End Sub

        Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
            TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
        End Sub
    End Class

(为演示添加一个多行文本框和一个按钮到这个表单)

工具.vb

    Imports System
    Imports System.Threading
    Imports System.IO.Compression

    Public Class Tools
    #Region "Zip"
        Private _zip As System.IO.Compression.ZipFile
        Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
        Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)

        Public Class ZipInfo
            Public Property ZipFile As String
            Public Property Path As String
        End Class


        Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Destination
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Folder
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Shared Sub ThreadUnzip(ZipInfo As Object)
            RaiseEvent UnzipComplete(ZipInfo)
        End Sub

        Shared Sub ThreadZip(ZipInfo As Object)
            RaiseEvent ZipComplete(ZipInfo)
        End Sub

    #End Region

    End Class

这段代码应该做什么,如下:

在 Button1_Click 上,向线程池添加 9 个项目 在每个线程完成时(顺序无关),引发提升到 Form1 的事件

Form1 上引发的事件应该是 UI 安全的,因此我可以使用传递给文本框中的 ZipCompleted / UnzipCompleted 事件的信息。这应该是通用的,这意味着引发事件的函数应该是可重用的,并且不会直接调用表单。 (又名,我不希望 Tools.vb 中的“自定义”子或函数调用 Form1.vb 上的特定元素。通过将类添加到我的项目然后在下输入任何“自定义”表单代码,这应该是通用且可重用的引发的事件(例如,当 Button1_Click 被引发时,即使它是线程化的,其他表单交互也不属于 Button1 对象/类的一部分——它们由编码器写入用户单击时引发的事件。

【问题讨论】:

【参考方案1】:

如果您想确保不直接了解您的 UI 的对象在 UI 线程上引发其事件,请使用 SynchronizationContext 类,例如

Public Class SomeClass

    Private threadingContext As SynchronizationContext = SynchronizationContext.Current

    Public Event SomethingHappened As EventHandler

    Protected Overridable Sub OnSomethingHappened(e As EventArgs)
        RaiseEvent SomethingHappened(Me, e)
    End Sub

    Private Sub RaiseSomethingHappened()
        If Me.threadingContext IsNot Nothing Then
            Me.threadingContext.Post(Sub(e) Me.OnSomethingHappened(DirectCast(e, EventArgs)), EventArgs.Empty)
        Else
            Me.OnSomethingHappened(EventArgs.Empty)
        End If
    End Sub

End Class

只要您在 UI 线程上创建该类的实例,它的 SomethingHappened 事件就会在 UI 线程上引发。如果没有 UI 线程,那么只会在当前线程上引发事件。

这里有一个更完整的例子,其中包括一个更简单的使用 Lambda 表达式的方法:

Imports System.Threading

Public Class Form1

    Private WithEvents thing As New SomeClass

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Me.thing.DoSomethingAsync()
    End Sub

    Private Sub thing_DoSomethingCompleted(sender As Object, e As IntegerEventArgs) Handles thing.DoSomethingCompleted
        MessageBox.Show(String.Format("The number is 0.", e.Number))
    End Sub

End Class


''' <summary>
''' Raises events on the UI thread after asynchronous tasks, assuming the instance was created on a UI thread.
''' </summary>
Public Class SomeClass

    Private ReadOnly threadingContext As SynchronizationContext = SynchronizationContext.Current

    Public Event DoSomethingCompleted As EventHandler(Of IntegerEventArgs)

    ''' <summary>
    ''' Begin an asynchronous task.
    ''' </summary>
    Public Sub DoSomethingAsync()
        Dim t As New Thread(AddressOf DoSomething)

        t.Start()
    End Sub

    Protected Overridable Sub OnDoSomethingCompleted(e As IntegerEventArgs)
        RaiseEvent DoSomethingCompleted(Me, e)
    End Sub

    Private Sub DoSomething()
        Dim rng As New Random
        Dim number = rng.Next(5000, 10000)

        'Do some work.
        Thread.Sleep(number)

        Dim e As New IntegerEventArgs With .Number = number

        'Raise the DoSomethingCompleted event on the UI thread.
        Me.threadingContext.Post(Sub() OnDoSomethingCompleted(e), Nothing)
    End Sub

End Class


Public Class IntegerEventArgs
    Inherits EventArgs

    Public Property Number() As Integer

End Class

【讨论】:

试试这个。关于子“RaiseSomethingHappened”中的“e”值的问题......这是从哪里来的,因为我没有看到任何可接受的参数。 Sub(e) 部分是 Lambda 表达式的声明。 SynchronizationContext.Post 的第一个参数是SendOrPostCallback 类型,它是Sub 的委托,其中一个参数类型为ObjectOnSomethingHappened 方法没有该签名,因此您不能直接使用它来创建委托。您需要另一种具有适当签名的方法,并且使用 Lambda 比声明命名方法更简单。我将添加一个额外的代码示例,该示例使用命名方法以使正在发生的事情更加明显。 您必须重新编写整个代码才能使用它。我们为您提供了非常简单的答案。 @TMcKeown,不。您只需要在Tools 类中添加一些简短的方法。坦率地说,OnZipCompleteOnUnzipComplete 方法应该已经存在了。 我确切地知道使用您的解决方案需要什么,这不是必需的,即使使用您的解决方案,当 SyncContext 不是 UI 线程时他需要处理 Invoke 时,他也只需要处理 Invoke .【参考方案2】:

您应该从 Form 注册到 Tools 类的事件(您已经定义了这些事件),当然实际事件将在非 UI 线程下触发,因此它在回调期间执行的代码只会是能够通过Invoke()更新用户界面

您想简单地在Tools 类中引发事件,Invoke 需要完成,因为您想更新 UI,Tools 类应该关注这一点。

像这样改变你的事件处理:

Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
   TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
End Sub

从视图注册到事件:(这将在 Button1_Click 事件中进行

AddHandler t.UnzipComplete, AddressOf t_UnzipComplete

确保您只注册一次活动

【讨论】:

我正在寻找课堂上更独立的东西。 哪个班?我关于引发事件的建议是你应该怎么做 那里,我的回答解释了需要什么。 有没有办法从 Tools 类中调用或以某种方式满足要求,以便在 raiseevent 命令之前或期间对 UI 友好? 不,你不想那样做。您想简单地引发事件,需要完成 Invoke 因为您想更新 UI,Tools 类不应该关心这一点。【参考方案3】:

这能解决您的问题吗?

Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
    If TextBox1.InvokeRequired Then
        TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
    Else
        TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
    End If
End Sub

您可以创建回调以更安全的方式进行调用。像这样的:

Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String, _
    ByVal SafeCallback As Action(Of ZipInfo))

然后调用代码会这样做:

t.Unzip("file 1", "foo", Sub (zi) TextBox1.Invoke(Sub () t_UnzipComplete(zi)))

我个人认为在事件处理程序上调用更好——也更传统——但你可以这样做。

【讨论】:

有趣。无论如何我可以强制 Invoke 在 raiseevent 行而不是在事件处理程序下完成? @SanuelJackson - 您需要引用 UI 对象(即TextBox1)才能调用.Invoke(...)。将您的类传递给引用将是不好的封装。 不,你不会想把那种代码放在 Tools 类中,这个 UI 代码属于 UI @SanuelJackson - 唯一可行的方法是通过工具类可以调用的委托而不是实际的 UI 元素。但是,这仍然可能会导致您的代码依赖问题。 @SanuelJackson - 您对***.com/questions/2026355/… 的引用与我的建议无关。我建议您将 Action(Of Tools.ZipInfo) 传递给您的 UnZip 方法,该方法被调用而不是引发事件。您从 UI 传递的委托需要执行调用。【参考方案4】:

好的,这就是我结合所有为这个问题做出贡献的人提供的信息得出的结论——所有优秀且非常有用的答案,帮助我找到了最终的解决方案。理想情况下,我希望这是一个直接的“类”,但我可以为此目的接受 UserControl。如果有人可以接受这个并在课堂上做完全相同的事情,那肯定会赢得我的选票。现在,我真的要考虑投给哪一个了。

这里是更新后的 Tools.vb

    Imports System
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.IO.Compression

    Public Class Tools
        Inherits UserControl
    #Region "Zip"
        Private _zip As System.IO.Compression.ZipFile

        Private threadingContext As SynchronizationContext = SynchronizationContext.Current

        Private Delegate Sub EventArgsDelegate(ByVal e As ZipInfo)

        Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
        Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)

        Public Class ZipInfo
            Public Property ZipFile As String
            Public Property Path As String
        End Class


        Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Destination
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Folder
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Private Sub ThreadUnzip(ZipInfo As Object)
            If Me.InvokeRequired Then
                Me.Invoke(New EventArgsDelegate(AddressOf ThreadUnzip), ZipInfo)
            Else
                RaiseEvent UnzipComplete(ZipInfo)
            End If
        End Sub

        Private Sub ThreadZip(ZipInfo As Object)
            If Me.InvokeRequired Then
                Me.Invoke(New EventArgsDelegate(AddressOf ThreadZip), ZipInfo)
            Else
                RaiseEvent ZipComplete(ZipInfo)
            End If
        End Sub
    #End Region
    End Class

如果您将它放在 Form1.vb 上,并选择/激活 UnzipComplete/ZipComplete 事件,您会发现它们将与 UI 线程交互,而无需从 Form 传递 Sub 或 Invoke 等。它也是通用的,这意味着它不知道您将与哪些表单元素进行交互,因此不需要显式调用,例如 TexBox1.Invoke() 或其他特定于元素的调用。

【讨论】:

您不需要将其设为 UserControl.... 为什么不直接在表单中的 UnzipComplete 事件中调用 Invoke @TMcKeown - 我希望在控件/类中保持 100% 的线程功能,而无需在类的每个实例上调用任何额外的东西。想象一下,如果在每个 Button1_Click 上,您必须编写 5 行额外的代码......我觉得这个类应该是自包含的和 UI 感知的(根据 OP)。本质上,这个类的唯一目的是处理背景内容,而我的 UI 代码仅作为 UI 逻辑。 想象一下,如果你不得不把每个类都变成一个 UserControl,因为程序员不想正确编码。 ;) 但说真的,这是编程,这是你必须做的。如果有人想使用您的生成事件的类,那么您可以正确处理它们......这就是它的完成方式,任何事件生成类都会遇到同样的问题。 我同意你的看法。需要其他代码的示例是在更新大量对象时锁定/解锁元素。我只是不觉得 UI 是处理从后台线程到 UI 线程的切换的地方,这可以以可重用的方式完成 - 编码中的一个关键点是使您的代码更具可移植性/可重复使用。 我没有关注,听起来你在不应该的地方有代码,并且让这个类成为用户控件使其更便携?祝你好运...

以上是关于VB.NET(WinForms)中带参数的安全线程池队列的主要内容,如果未能解决你的问题,请参考以下文章

如何在 VB.Net winforms 应用程序中找到 main() 入口点?

VB.NET 中的线程安全变量

所有循环崩溃winforms vb.net?

WinForms - VB.NET - 为两个不同的列表框传递与数据源相同的列表会产生问题

控制器中带有操作的 GET 和 POST - VB.NET 2012 MVC 4

如何通过 vb.net 让其他应用程序(计算器)在 winforms 中脱颖而出?