【问题标题】:How can I run code in a background thread and still access the UI?如何在后台线程中运行代码并仍然访问 UI?
【发布时间】:2018-01-15 05:59:08
【问题描述】:

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

1)form1 调用 form2 并显示出来。

2)form1 开始一个while循环。

3)在while循环中,数据被馈送到frm2中的listbox1

现在一切都可以在 windows 10 上运行,while 循环可以根据需要运行而不会出现任何问题,窗口可以失去焦点并重新获得焦点 不会显示任何 @ 987654322@

但是,当我将软件带到运行 windows 7 的朋友计算机上时,安装所有必需的框架和 Visual Studio 本身,从 .sln 中运行它调试模式,并在同一个文件夹上做同样的搜索,结果是:

1)只要form 2不失去焦点,while循环就可以顺利运行 (在 Windows 10 上不会发生的事情)

2) 当我点击屏幕上的任意位置时,软件会失去焦点 导致1)发生(黑屏\白屏\无响应等)

3) 如果我等待循环所需的时间并且不要点击其他任何地方 它保持平稳运行,更新标签应该与 找到的文件数量.. 甚至以 100% 成功 完成循环 (再次,除非我点击某处)

代码示例:

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 不支持从 while 循环进行实时 ui(标签)更新 从按钮调用...

2) Windows 7 可能支持新的 线程运行相同的循环

我认为如果我在一个线程中运行所有代码也许用户界面将保持响应

(顺便说一下,windows 10 中的 UI 没有响应,但我仍然看到 标签刷新并且在失去焦点时不会崩溃..

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

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

我必须提到一个事实,即循环在没有响应式 UI 的情况下在 Windows 10 上运行意味着我无法点击任何按钮,但我可以 仍然看到标签刷新但在 Windows 7 上一切正常 除非我点击某个地方,无论我在哪里点击窗口循环 崩溃

我正在使用框架 4.6.2 开发人员

【问题讨论】:

  • 请提供可重现的代码示例,以便人们可以帮助您。
  • 我添加了一个代码示例 :)
  • 它在 Windows 10 中很奇怪,即使 UI 没有响应,一切都运行顺利,而在 Windows 7 中,没有响应 UI 的情况下,一切都运行顺利,除非我点击某个地方,否则一切都没有响应并崩溃。我认为这个问题可以在没有异步调用的情况下解决
  • 必须使用Async/Await、并行性、任务或多线程来减轻您的 UI 线程的繁重工作和 能够更新它。没有其他(好的)方法可以做到这一点。
  • 这听起来确实令人反感,所以这就是为什么我说了我所说的,但我很抱歉我不知道我在想什么......这是和朋友开玩笑......

标签: vb.net multithreading windows-7 crash focus


【解决方案1】:

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

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

简单地说,Application.DoEvents() 是一种肮脏的解决方法,它使您的 UI 看起来响应迅速,因为它强制 UI 线程处理所有当前可用的窗口消息。 WM_PAINT 是您的窗口重绘的原因之一。

然而,这也有一些弊端......例如:

  • 如果您要在此“后台”过程中关闭表单,很可能会引发错误。

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

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

当然,替代品是适当的多线程(或任务并行库,无论您喜欢哪个)。常规的多线程实际上并不难实现。


基础知识

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

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

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

然后你只需调用Start(),你就有一个正在运行的后台线程!

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


访问 UI 线程

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

要编组对 UI 线程的调用,您可以使用 Control.Invoke()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


使用 InvokeIfRequired() 从 UI 返回对象/数据

使用我的InvokeIfRequired() 扩展方法,您还可以以简单的方式从 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


希望这会有所帮助!

【讨论】:

    【解决方案2】:

    您的应用程序冻结的原因是您正在 UI 线程上完成所有工作。查看异步和等待。它在后台使用线程,但更容易管理。这里有一个例子:

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

    【讨论】:

    • 我在主要问题上附加了一个代码示例,您有什么建议可以让它运行异步和等待而不会出现错误:“frm2.ListBox1.Items.AddRange”或“frm2” .Label1.Refresh()" 或 "frm2.Label1.Text="asd" "
    • 我很高兴听到大师们关于如何改进当前代码以提高效率和减少错误的一些建议:)
    • 使用 Invoke 从其他线程更新控件。 stackoverflow.com/a/17631369/2319909
    • 感谢您提供的提示,您可以在 vb.net 中提供一个示例吗?
    • 在我的情况下,将 c# 转换为 .net 永远不会结束,我尝试在一个线程中进行此操作并且 aSync 我无法使其工作..(当我尝试更新列表框或刷新标签+更新标签值)请查看我的代码告诉我您是否看到了一种异步运行它并更新列表框+标签的方法
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-16
    • 2020-10-13
    • 2011-06-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多