vb.net 委托和调用 - 多线程

vb.net delegate and invoke - Multithread

我正在尝试了解委托和调用的工作原理。

所以我构建了一个带有标签和按钮的表单。

当有人点击按钮时,文本变为 "Stop" 并且计数器开始计数。此计数器应显示在标签上。这是我的代码:

Public Class Form1

  Private t1 As Thread
  Private sek_ As Integer = 0

  Private Sub btn_read_Click(sender As Object, e As EventArgs) Handles btn_read.Click


    If btn_read.Text = "Read" Then

        btn_read.Text = "Stop"
        t1 = New Thread(AddressOf stopw_)
        t1.Start()

    Else
        lbl_stopw_.Text = ""
        btn_read.Text = "Read"

    End If

  End Sub

  Private Delegate Sub stopw_D()

  Private Sub stopw_()

    Do While btn_read.Text = "Stop"
        sek_ = sek_ + 1

        If lbl_stopw_.InvokeRequired Then
            lbl_stopw_.Invoke(New stopw_D(AddressOf stopw_))
        Else
            lbl_stopw_.Text = sek_
        End If

        Thread.Sleep(1000)

    Loop

    sek_ = 0

    If t1.IsAlive Then t1.Abort()

  End Sub

End Class

如果我开始调试,表单仍然冻结并且标签没有更新。如果我删除所有委托并调用内容并使用 Me.CheckForIllegalCrossThreadCalls = False 它的工作。

我做错了什么?

Control.Invoke() 用于在创建控件的同一线程上执行方法。由于代码在每个线程中一次只能执行一行,因此在控件的线程上执行一个方法将导致该方法为 "queued",直到该线程中该方法之前的其他代码完成为止。这使得它 thread-safe 因为不会有并发问题。

Delegate is simply a class holding the pointer to a method. It exists so that you can use methods as if they were ordinary objects (in this case you pass it to a function). The AddressOf 运算符是创建委托的快速方法。

现在,您的代码中存在一些问题。首先,您不应该尝试从后台线程访问或修改 ANY UI 元素。每当你想修改或检查一个控件时,你必须总是调用。

更具体地说,我说的是您的 While-循环:

'You can't check the button here without invoking.
Do While btn_read.Text = "Stop"

最好创建一个 Boolean 变量来指示线程何时应该 运行。

Private t1 As Thread
Private sek_ As Integer = 0
Private ThreadActive As Boolean = False

在启动 thead 之前将 ThreadActive 设置为 True,然后在线程的 While 循环中检查:

Do While ThreadActive

现在,还有一个问题。您的 UI 因以下原因冻结:

If lbl_stopw_.InvokeRequired Then
    lbl_stopw_.Invoke(New stopw_D(AddressOf stopw_))

从不调用相同的线程运行上的方法!这样做会重新开始处理,但是在 UI 线程 上。您的循环使 UI 线程完全忙碌,这就是它不重绘自身的原因。

所以当你要更新某些东西时,总是调用一个单独的方法。如果您的目标是 .NET 4.0 或更高版本,您可以使用 lambda expressions 进行快速的内联委托:

If lbl_stopw_.InvokeRequired Then
    lbl_stopw_.Invoke( _
        Sub()
            lbl_stopw_.Text = sek_
        End Sub)
Else
    lbl_stopw_.Text = sek_
End If

但是,如果您的目标是 .NET 3.5 或更低版本,您必须坚持使用委托的正常方式:

'Outside your thread.
Private Delegate Sub UpdateLabelDelegate(ByVal Text As String)

Private Sub UpdateLabel(ByVal Text As String)
    lbl_stopw_.Text = Text
End Sub

'In your thread.
If lbl_stopw_.InvokeRequired Then
    lbl_stopw_.Invoke(New UpdateLabelDelegate(AddressOf UpdateLabel), sek_)
Else
    UpdateLabel(sek_)
End If

或者,为了最大限度地减少您必须编写的代码量,您可以创建一个 extension method 来为您执行调用:

Imports System.Runtime.CompilerServices

Public Module Extensions
    <Extension()> _
    Public Sub InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object())
        If Parameters Is Nothing OrElse _
            Parameters.Length = 0 Then Parameters = Nothing 'If Parameters is null or has a length of zero then no parameters should be passed.
        If Control.InvokeRequired = True Then
            Control.Invoke(Method, Parameters)
        Else
            Method.DynamicInvoke(Parameters)
        End If
    End Sub
End Module

用法,.NET 4.0 或更高版本

lbl_stopw_.InvokeIfRequired( _
    Sub()
        lbl_stopw_.Text = sek_
    End Sub)

用法,.NET 3.5 或更低版本

lbl_stopw_.InvokeIfRequired(New UpdateLabelDelegate(AddressOf UpdateLabel), sek_)

使用此扩展方法时,您无需在所有地方编写 InvokeRequired 检查:

Do While btn_read.Text = "Stop"
    sek_ = sek_ + 1

    lbl_stopw_.InvokeIfRequired(New UpdateLabelDelegate(AddressOf UpdateLabel), sek_)

    Thread.Sleep(1000)
Loop

最后,这是不必要的:

If t1.IsAlive Then t1.Abort()

线程在到达 If 语句时将始终处于活动状态,因为它尚未退出 stopw_ 方法。但是一旦线程退出该方法,它就会正常结束,所以没有理由调用 Abort().

回答有点长,希望对您有所帮助!