How can I run code in a background thread and stil

2019-01-02 16:20发布

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

2条回答
宁负流年不负卿
2楼-- · 2019-01-02 16:37

The reason your application is freezing is that you are doing all the work on the UI thread. Check out Async and Await. It uses threading in the background but makes it way easier to manage. An example here:

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

查看更多
若你有天会懂
3楼-- · 2019-01-02 16:55

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:

'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!

查看更多
登录 后发表回答