如何在后台线程中运行代码并仍然访问UI? [英] How can I run code in a background thread and still access the UI?

查看:94
本文介绍了如何在后台线程中运行代码并仍然访问UI?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用.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上看到标签刷新BUT,但一切正常 除非我在任何地方单击,否则无论我在Windows的哪个位置单击循环 崩溃

我使用框架4.6.2开发人员

解决方案

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

请参阅此博客文章:保持UI响应能力和Application.DoEvents的危险 .

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

但是这有一些缺点……例如:

  • 如果您在此后台"过程中关闭表单,则很可能会引发错误.

  • 另一个缺点是,如果通过单击按钮来调用ScanButtonInForm1()方法,则您可以再次单击该按钮(除非您设置了Enabled = False)并再次启动该过程,带给我们又一个背面:

  • 启动的Application.DoEvents()循环越多,占用的UI线程就越多,这将导致CPU使用率迅速上升.由于每个循环都在同一线程中运行,因此您的处理器无法在不同的内核或线程上调度工作,因此您的代码将 始终 在一个内核上运行,占用的CPU数量与可能.

当然,替换是适当的多线程(或任务并行库,以您喜欢的为准).常规的多线程实际上并不难实现.


基础

为了创建一个新线程,您只需要声明一个 IsBackground属性 True,如果您希望它在程序关闭时自动关闭(否则它将保持程序打开直到线程完成). /p>

然后,您只需致电 Start() ,并且您有一个正在运行的后台线程!

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


访问UI线程

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

要对UI线程进行Marhsal调用,请使用

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

但是,从 Visual Basic 2010(10.0)及更高版本 开始,您可以使用

现在您所要做的就是键入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,可以创建

现在,您只需要在要访问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


希望这会有所帮助!

I made a file search program in visual studio on windows 10 using .net lang, My problem starts from form1 with a "dim frm2 as form2 = new form2" call, after the new form being shown i start a while loop on form1 that feeds data into a listbox in form 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

Now everything works on windows 10, the while loop can run as much as it needs without any trouble, the window can loose focus and regain focus without showing any "Not Responding.." msgs or white\black screens..

But, when i take the software to my friend computer which is running windows 7, install all required frameworks and visual studio itself, run it from the .sln in debug mode, and do the same search on the same folder the results are:

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)

Code Example:

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

My conclusions was simple!

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

i think mabye if i run all the code in a thread mabye the ui will remain responsive

(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..)

so i know how to do that but i also know that if i do that a thread will not be able to update a listbox or a label in a form and refresh it..

so the thread will need to update an external file with the data and the form2 will need to read that data live from the file but will it make the same problems? i have no idea what to do.. can use some help and tips. THANK YOU!

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

解决方案

While I'm glad you found a solution, I advise against using Application.DoEvents() because it is bad practice.

Please see this blog post: Keeping your UI Responsive and the Dangers of Application.DoEvents.

Simply put, Application.DoEvents() is a dirty workaround that makes your UI seem responsive because it forces the UI thread to handle all currently available window messages. WM_PAINT is one of those messages which is why your window redraws.

However this has some backsides to it... For instance:

  • If you were to close the form during this "background" process it would most likely throw an error.

  • Another backside is that if the ScanButtonInForm1() method is called by the click of a button you'd be able to click that button again (unless you set Enabled = False) and starting the process once more, which brings us to yet another backside:

  • The more Application.DoEvents()-loops you start the more you occupy the UI thread, which will cause your CPU usage to rise rather quickly. Since every loop is run in the same thread your processor cannot schedule the work over different cores nor threads, so your code will always run on one core, eating as much CPU as possible.

The replacement is, of course, proper multithreading (or the Task Parallel Library, whichever you prefer). Regular multithreading actually isn't that hard to implement.


The basics

In order to create a new thread you only need to declare an instance of the Thread class and pass a delegate to the method you want the thread to run:

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

...then you should set its IsBackground property to True if you want it to close automatically when the program closes (otherwise it keeps the program open until the thread finishes).

Then you just call Start() and you have a running background thread!

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


Accessing the UI thread

The tricky part about multithreading is to marshal calls to the UI thread. A background thread generally cannot access elements (controls) on the UI thread because that might cause concurrency issues (two threads accessing the same control at the same time). Therefore you must marshal your calls to the UI by scheduling them for execution on the UI thread itself. That way you will no longer have the risk of concurrency because all UI related code is run on the UI thread.

To marhsal calls to the UI thread you use either of the Control.Invoke() or Control.BeginInvoke() methods. BeginInvoke() is the asynchronous version, which means it doesn't wait for the UI call to complete before it lets the background thread continue with its work.

One should also make sure to check the Control.InvokeRequired property, which tells you if you already are on the UI thread (in which case invoking is extremely unnecessary) or not.

The basic InvokeRequired/Invoke pattern looks like this (mostly for reference, keep reading below for shorter ways):

'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) creates a new instance of the UpdateTextBoxDelegate that points to our UpdateTextBox method (the method to invoke on the UI).

However as of Visual Basic 2010 (10.0) and above you can use Lambda expressions which makes invoking much easier:

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

Now all you have to do is type Sub() and then continue typing code like if you were in a regular method:

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

And that's how you marshal calls to the UI thread!


Making it simpler

To make it even more simple to invoke to the UI you can create an Extension method that does the invoking and InvokeRequired check for you.

Place this in a separate code file:

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

Now you only need to call this single method when you want to access the UI, no additional If-Then-Else required:

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


Returning objects/data from the UI with InvokeIfRequired()

With my InvokeIfRequired() extension method you can also return objects or data from the UI thread in a simple manner. For instance if you want the width of a label:

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


Example

The following code will increment a counter that tells you for how long the thread has run:

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


Hope this helps!

这篇关于如何在后台线程中运行代码并仍然访问UI?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆