我如何 运行 在后台线程中编写代码并仍然访问 UI?

How can I run code in a background thread and still access the UI?

我在 visual studio on windows 10 使用 .net lang 创建了一个文件搜索程序, 我的问题从带有“dim frm2 as form2 = new form2”调用的 form1 开始, 在显示新表单后,我在 form1 上开始一个 while 循环,将数据馈送到表单 2 中的列表框:

1)form1 call form2 and show it.

2)form1 start a while loop.

3)inside the while loop data being fed to listbox1 in frm2

现在一切都在 windows 10 上运行,while 循环可以 运行 随心所欲,window可以在不显示任何 "Not Responding.." msgs or white\black screens..

的情况下 失去焦点并重新获得焦点

但是, 当我把软件带到我朋友的电脑上 运行ning windows 7 ,安装所有 required 框架和 visual studio 本身,运行 在调试模式下从 .sln 中安装它,并在同一文件夹上进行相同的搜索,结果是:

1) the while loop runs smoothly as long as form 2 dont loose focus (something that doesnt happen on windows 10)

2) when i click anywhere on the screen the software loose focus what causes 1) to happen (black screen\white screen\not responding etc..)

3) if i wait the time needed for the loop and dont click anywhere else it keeps running smoohtly, updating a label like it should with the amount of files found.. and even finish the loop with 100% success (again unless i click somewhere)

代码示例:

Sub ScanButtonInForm1()
    Dim frm2 As Form2 = New Form2
    frm2.Show()
    Dim AlreadyScanned As HashSet(Of String) = New HashSet(Of String)
    Dim stack As New Stack(Of String)
    stack.Push("...Directoy To Start The Search From...")
    Do While (stack.Count > 0)
        frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
        frm2.Label4.Refresh()
        Dim ScanDir As String = stack.Pop
        If AlreadyScanned.Add(ScanDir) Then
            Try
                Try
                    Try
                        Dim directoryName As String
                        For Each directoryName In System.IO.Directory.GetDirectories(ScanDir)
                            stack.Push(directoryName)
                            frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
                            frm2.Label4.Refresh()
                        Next
                        frm2.ListBox1.Items.AddRange(System.IO.Directory.GetFiles(ScanDir, "*.*", System.IO.SearchOption.AllDirectories))
                    Catch ex5 As UnauthorizedAccessException
                    End Try
                Catch ex2 As System.IO.PathTooLongException
                End Try
            Catch ex4 As System.IO.DirectoryNotFoundException
            End Try
        End If
    Loop
End Sub

我的结论很简单!

1) windows 7 dont support live ui (label) update from a while loop called from a button...

2) windows 7 could possibly support a new thread running the same loop

我想也许如果我 运行 线程中的所有代码也许 ui 将保持响应

(by the way the UI is not responsive in windows 10 but i still see the label refresh and nothing crashes when form loose focus..)

所以我知道该怎么做,但我也知道如果我这样做,线程将无法更新表单中的列表框或标签并刷新它..

所以线程需要用数据更新外部文件,而 form2 需要从文件中实时读取数据,但它会产生同样的问题吗?我不知道该怎么做。可以使用一些帮助和提示。 谢谢!

I must menttion the fact that the loop is working on windows 10 without a responsive UI means i cant click on any button but i can still see the label refresh BUT on windows 7 everything works the same UNLESS i click somewhere, no matter where i click on windows the loop crashes

im using framework 4.6.2 developer

您的应用程序冻结的原因是您正在 UI 线程上执行所有工作。查看异步和等待。它在后台使用线程,但更易于管理。此处示例:

https://stephenhaunts.com/2014/10/14/using-async-and-await-to-update-the-ui-thread/

虽然很高兴您找到了解决方案,但我建议您不要使用 Application.DoEvents(),因为 这是不好的做法

请参阅此博客 post:Keeping your UI Responsive and the Dangers of Application.DoEvents

简而言之,Application.DoEvents() 是一种肮脏的解决方法,它使您的 UI 看起来响应迅速,因为它强制 UI 线程处理所有当前可用的 window 消息。 WM_PAINT 是其中一条消息,这就是为什么您的 window 重绘。

然而这有一些缺点...例如:

  • 如果您在此 "background" 过程中关闭表单,很可能会引发错误。

  • 另一个缺点是,如果通过单击按钮调用 ScanButtonInForm1() 方法,您将能够再次单击该按钮(除非您设置 Enabled = False)并再次开始这个过程,这将我们带到了另一个背后:

  • 启动的Application.DoEvents()循环越多,占用UI线程的次数就越多,这将导致您的CPU使用率上升得相当快。由于每个循环都是 运行 在同一个线程中,您的处理器无法在不同的内核或线程上安排工作,因此您的代码将 always 运行在一个核心上,尽可能吃CPU。

替代品当然是适当的多线程(或 Task Parallel Library,随您喜欢)。常规多线程实际上并不难实现。


基础知识

为了创建一个新线程,您只需声明一个 Thread class 的实例并将委托传递给您希望线程使用的方法 运行:

Dim myThread As New Thread(AddressOf <your method here>)

...如果你希望它在程序关闭时自动关闭,你应该将它的IsBackground property设置为True(否则它会保持程序打开直到线程完成)。

那么你只需调用 Start() 就可以得到一个 运行ning 后台线程!

Dim myThread As New Thread(AddressOf myThreadMethod)
myThread.IsBackground = True
myThread.Start()


正在访问 UI 线程

关于多线程的棘手部分是编组对 UI 线程的调用。后台线程通常无法访问 UI 线程上的元素(控件),因为这可能会导致并发问题(两个线程同时访问同一控件)。因此,您必须通过安排 在 UI 线程本身 上执行来编组对 UI 的调用。这样您将不再有并发风险,因为所有 UI 相关代码都在 UI 线程上 运行。

要编组对 UI 线程的调用,您可以使用 Control.Invoke() or Control.BeginInvoke() 方法之一。 BeginInvoke() 异步 版本,这意味着它不会等待 UI 调用完成就让后台线程继续其工作。

还应确保检查 Control.InvokeRequired property,它会告诉您是否已经在 UI 线程上(在这种情况下调用是 非常 不必要) 或不.

基本的 InvokeRequired/Invoke 模式如下所示(主要供参考,请继续阅读下文以了解更简短的方法):

'This delegate will be used to tell Control.Invoke() which method we want to invoke on the UI thread.
Private Delegate Sub UpdateTextBoxDelegate(ByVal TargetTextBox As TextBox, ByVal Text As String)

Private Sub myThreadMethod() 'The method that our thread runs.
    'Do some background stuff...

    If Me.InvokeRequired = True Then '"Me" being the current form.
        Me.Invoke(New UpdateTextBoxDelegate(AddressOf UpdateTextBox), TextBox1, "Status update!") 'We are in a background thread, therefore we must invoke.
    Else
        UpdateTextBox(TextBox1, "Status update!") 'We are on the UI thread, no invoking required.
    End If

    'Do some more background stuff...
End Sub

'This is the method that Control.Invoke() will execute.
Private Sub UpdateTextBox(ByVal TargetTextBox As TextBox, ByVal Text As String)
    TargetTextBox.Text = Text
End Sub

New UpdateTextBoxDelegate(AddressOf UpdateTextBox) 创建 UpdateTextBoxDelegate 的一个新实例,它指向我们的 UpdateTextBox 方法(在 UI 上调用的方法)。

然而,从 Visual Basic 2010 (10.0) 及更高版本 开始,您可以使用 Lambda expressions 来调用 容易得多

Private Sub myThreadMethod()
    'Do some background stuff...

    If Me.InvokeRequired = True Then '"Me" being the current form.
        Me.Invoke(Sub() TextBox1.Text = "Status update!") 'We are in a background thread, therefore we must invoke.
    Else
        TextBox1.Text = "Status update!" 'We are on the UI thread, no invoking required.
    End If

    'Do some more background stuff...
End Sub

现在您所要做的就是键入 Sub(),然后像使用常规方法一样继续键入代码:

If Me.InvokeRequired = True Then
    Me.Invoke(Sub()
                  TextBox1.Text = "Status update!"
                  Me.Text = "Hello world!"
                  Label1.Location = New Point(128, 32)
                  ProgressBar1.Value += 1
              End Sub)
Else
    TextBox1.Text = "Status update!"
    Me.Text = "Hello world!"
    Label1.Location = New Point(128, 32)
    ProgressBar1.Value += 1
End If

这就是您编组对 UI 线程的调用的方式!


让它更简单

为了甚至更容易调用UI,您可以创建一个Extension method来执行调用和InvokeRequired检查给你。

将其放在单独的代码文件中:

Imports System.Runtime.CompilerServices

Public Module Extensions
    ''' <summary>
    ''' Invokes the specified method on the calling control's thread (if necessary, otherwise on the current thread).
    ''' </summary>
    ''' <param name="Control">The control which's thread to invoke the method at.</param>
    ''' <param name="Method">The method to invoke.</param>
    ''' <param name="Parameters">The parameters to pass to the method (optional).</param>
    ''' <remarks></remarks>
    <Extension()> _
    Public Function InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object()) As Object
        If Parameters IsNot Nothing AndAlso _
            Parameters.Length = 0 Then Parameters = Nothing

        If Control.InvokeRequired = True Then
            Return Control.Invoke(Method, Parameters)
        Else
            Return Method.DynamicInvoke(Parameters)
        End If
    End Function
End Module

现在访问UI时只需要调用这个方法,不需要额外的If-Then-Else:

Private Sub myThreadMethod()
    'Do some background stuff...

    Me.InvokeIfRequired(Sub()
                            TextBox1.Text = "Status update!"
                            Me.Text = "Hello world!"
                            Label1.Location = New Point(128, 32)
                        End Sub)

    'Do some more background stuff...
End Sub


从 UI 和 InvokeIfRequired()

返回 objects/data

使用我的 InvokeIfRequired() 扩展方法,您还可以以简单的方式 return 来自 UI 线程的对象或数据。例如,如果您想要标签的宽度:

Dim LabelWidth As Integer = Me.InvokeIfRequired(Function() Label1.Width)


例子

以下代码将递增一个计数器,告诉您线程有多长时间 运行:

Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    Dim CounterThread As New Thread(AddressOf CounterThreadMethod)
    CounterThread.IsBackground = True
    CounterThread.Start()

    Button1.Enabled = False 'Make the button unclickable (so that we cannot start yet another thread).
End Sub

Private Sub CounterThreadMethod()
    Dim Time As Integer = 0

    While True
        Thread.Sleep(1000) 'Wait for approximately 1000 ms (1 second).
        Time += 1

        Me.InvokeIfRequired(Sub() Label1.Text = "Thread has been running for: " & Time & " seconds.")
    End While
End Sub


希望对您有所帮助!