如何在后台线程中运行代码并仍然可以访问UI?

时间:2017-08-07 20:29:16

标签: 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)在frm2

中输入到listbox1的while循环数据中

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

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

  

1)只要表格2没有松散焦点,while循环就会顺利运行   (在Windows 10上没有发生的事情)

     

2)当我点击屏幕上的任何地方时,软件会松散焦点   导致 1)发生(黑屏\白屏\无响应等)。

     

3)如果我等待循环所需的时间,不要点击其他地方   它一直保持运行,更新标签就像它应该   发现文件的数量..甚至以 100%成功完成循环   (再次,除非我点击某处)

代码示例:

"Not Responding.." msgs or white\black screens..

我的结论很简单!

  

1) windows 7 不支持while循环中的live ui(label)更新   从按钮调用......

     

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

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

  

通过UI在 Windows 10 中没有响应的方式,但我仍然看到   标签刷新,形成松散焦点时没有崩溃 ..

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

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

  

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

     

即时使用框架4.6.2开发人员

2 个答案:

答案 0 :(得分:8)

虽然我很高兴你找到了解决方案,但我建议不要使用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线程上运行。

对于使用Control.Invoke()Control.BeginInvoke()方法之一的UI线程的marhsal调用。 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,这样可以更轻松地调用更多:< / p>

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

现在,您只需要在访问用户界面时调用此单一方法,无需额外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


希望这有帮助!

答案 1 :(得分:1)

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

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