vb.net - 如何在后台线程中运行代码并仍然访问UI?

标签 vb.net multithreading windows-7 crash focus

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


1)form1调用form2并显示它。

2)form1开始一个while循环。

3)在while循环数据中馈入frm2中的listbox1


现在一切都可以在Windows 10上正常运行,while循环可以根据需要运行,而不会出现任何麻烦,该窗口可以失去焦点并重新获得焦点,而不会显示任何"Not Responding.." msgs or white\black screens..

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


1)只要表格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可能支持新
线程运行相同的循环


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


(顺便说一下,UI在Windows 10中没有响应,但我仍然看到
标签刷新,当焦点松动时没有崩溃。)


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

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


我必须提到以下事实:循环在Windows 10上没有响应UI的情况下工作,这意味着我无法单击任何按钮,但我可以
仍然在Windows 7上看到标签刷新按钮,但一切正常
除非我在任何地方单击,否则无论我在Windows的哪个位置单击循环
崩溃

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

最佳答案

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

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

简而言之,Application.DoEvents()是一种肮脏的解决方法,它使您的UI显得敏感,因为它强制UI线程处理所有当前可用的窗口消息。 WM_PAINT是这些消息之一,这就是窗口重绘的原因。

但是,这有一些缺点。例如:


如果要在此“后台”过程中关闭表单,则很可能会引发错误。
另一个缺点是,如果通过单击按钮来调用ScanButtonInForm1()方法,则可以再次单击该按钮(除非您设置了Enabled = False),然后再次启动该过程,这使我们回到了另一个缺点:
启动的Application.DoEvents()循环越多,您占用的UI线程就越多,这将导致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




希望这可以帮助!

关于vb.net - 如何在后台线程中运行代码并仍然访问UI?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45555204/

相关文章:

java - ConcurrencyHashMap 和 BashProcessing

java - Java 计划执行程序的未处理异常

c# - 从 AAC 文件中读取或写入评级

重启PC后Mysql表损坏

.net - 字典类型的问题,如果给定一个特定的文本然后得到等效的字典键或字典值?

vb.net - 如何使用 EWS API 从嵌套电子邮件中获取文件附件

c# - 如何清除字符串中的重复空格

java - ScheduledExecutorService Thread 创建多个线程

c# - 在 Windows Vista、Windows 7 中截屏,应用区域外有透明区域

vb.net - VB NET 后台 worker 问题