|
-
Feb 17th, 2016, 06:36 AM
#1
Thread Starter
Lively Member
Struggling with Form multithreading
Good morning
I really hope someone can take pity on me and give me some assistance, because I'm sitting here surrounded by the hair that I've torn out this morning.
I'm in the process of building a VB winform which allows the user to import data from a CSV file. It then performs a variety of tasks against the data that's been imported. Because the import file usually contains upwards of 8,000 records and the subsequent tasks can take quite a bit of time to complete, I've added a progress bar and a text box which I use as a status window to provide the user with details of where they are in the process.
Everything is working absolutely perfectly. However I've got the vast majority of the code I've written behind the form, so I want to look at pushing the code into separate classes so that there's as little code behind the form as I can get away with. However when I do that, the UI stops updating - if I step through the code I can see that the functions that should be updating the progress bar and status window appear to be working, but nothing is appearing on the UI.
Everything that I'm doing for this is being handled as a multithreaded task. So, when the user clicks the appropriate button on the UI, the code behind that button assigns the action as a background thread and starts it.
Can anyone provide me with some assistance? I'm seriously going bald here!
-
Feb 17th, 2016, 07:05 AM
#2
Hyperactive Member
Re: Struggling with Form multithreading
Hi, when you move threads into a class they effectively belong to that class and report their progress there (including when they finish). The best option I have found in this case is to pass a reference to the form/control into the thread or the Synchronization Context of that control/form. When it completes you can then invoke your changes back to the original control/form allowing you to access update the controls as normal.
E.g. something like this should work:
VB.NET Code:
Public Class Form1
Private Sub YourMethod()
' Create a thread and start it with the SynchronizationContext of the current control/form
Dim Worker As New ThreadWorker
Dim T As New Thread(AddressOf Worker.AsyncMethod)
T.Start(SynchronizationContext.Current)
End Sub
End Class
Public Class ThreadWorker
Public Sub AsyncMethod(ByVal state As Object)
' Do your async work
' Then run the final method on the same thread as the calling control/form by using it's SynchronizationContext
Dim Context As SynchronizationContext = CType(state, SynchronizationContext)
Context.Send(AddressOf AsyncMethodComplete, Nothing)
End Sub
Private Sub AsyncMethodComplete(state As Object)
'This code should run back on the control thread.
End Sub
End Class
HTH
-
Feb 17th, 2016, 08:03 AM
#3
Re: Struggling with Form multithreading
Here is another example. The form and some data is passed to the class which executes some worker tasks.
Code:
Public Class Example
Private theForm As Form1
Private theWorkers As New List(Of Task)
Public Sub New(aForm As Form1, data As List(Of Integer))
Me.theForm = aForm
For Each item As Integer In data
Dim wrkrt As Task = New Task(Sub()
Me.worker(item)
End Sub)
Me.theWorkers.Add(wrkrt)
wrkrt.Start()
Next
End Sub
Public Sub worker(dataitem As Integer)
Me.theForm.Invoke(Sub()
Me.theForm.ProgressBar1.PerformStep() 'started
End Sub)
Threading.Thread.Sleep(dataitem) 'the work
Me.theForm.Invoke(Sub()
'completed
Me.theForm.ProgressBar1.PerformStep()
Me.theForm.TextBox1.Text = dataitem.ToString
End Sub)
End Sub
Public Sub WaitAll()
Task.Factory.ContinueWhenAll(Me.theWorkers.ToArray,
Sub()
Debug.WriteLine("All done. {0} tasks completed", Me.theWorkers.Count)
End Sub)
End Sub
End Class
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
ProgressBar1.Minimum = 0
ProgressBar1.Maximum = 6
ProgressBar1.Value = ProgressBar1.Minimum
TextBox1.Text = ""
Dim testdata As New List(Of Integer) From {500, 2000, 4000}
Dim foo As New Example(Me, testdata)
foo.WaitAll() 'this does not block the UI
End Sub
-
Feb 17th, 2016, 10:28 AM
#4
Re: Struggling with Form multithreading
It's an easy topic, but a hard one. What I mean is, the concepts you need to know are pretty simple, but there's a fairly wide array of tricks you have to use to accomplish it. That's why both jay20aiii and dbasnett are "right", but their examples look completely different.
Here's the basic concept:
The first important thing to know is everything you do "belongs" to some thread. Sometimes you care about that.
When your application starts up, the UI is created and set up on a special thread we call either "the UI thread" or "the main thread". This thread is special because VB sets up some things needed for a UI application on it. If anything that tries to work with a control belongs to a different thread, the result is unpredictable and can range from "nothing happens" to "the application crashes".
So there are special techniques that let you say, "Make sure this code executes on the UI thread." Which technique you use depends a lot on how you came to be on another thread in the first place, or if you're using Windows Forms vs. WPF, and several other factors. So you have to know a few tricks. There are ways to design your forms and other classes to help mitigate this.
Show some code!
I can give very specific advice if I can see your code and how you end up creating worker threads. There's at least 5 ways to schedule work on other threads in .NET, and some of them are a lot easier than others. For example, here's an example using VS 2013 and later's Task Asynchronous Pattern with Async/Await:
Code:
Private Async Sub LoadData()
Dim fileParser As New yourFileParser()
Dim customerData = Await fileParser.ParseAsync(...)
For Each entry in customerData
Dim result = Await customerData.ProcessAsync(...)
UpdateListView(result)
UpdateProgress()
Next
End Sub
The neat thing about this pattern is that "Await" keyword. It says, "I know this method is going to do things on a worker thread, and I want to do things when it finishes. So when you get here, start that thread, then leave this method so the form can do other things. When the thread is finished, come back here on this same thread and continue." It's extremely elegant and eliminates 90% of the worries of threading.
But this is what it'd look like with the Event-Based Asynchronous Pattern, which is how BackgroundWorker functions:
Code:
Private Sub LoadData()
Dim fileParser As New YourFileParser()
AddHandler fileParser.ParseCompleted, AddressOf HandleParseCompleted
fileParser.ParseAsync(...)
End Sub
Private Sub HandleParseCompleted(ByVal sender As Object, ByVal e As ParseCompletedEventArgs)
Dim customerData = e.Results
Dim dataProcessor As New CustomerDataProcessor(customerData)
AddHandler dataProcessor.Progress, AddressOf HandleDataProgress
AddHandler dataProcessor.ProcessCompleted, AddressOf HandleProcessCompleted
dataProcessor.ProcessAsync(customerData)
End Sub
Private Sub HandleDataProgress(ByVal sender As Object, ByVal e As DataProgressEventArgs)
Dim result = e.Result
UpdateListView(result)
UpdateProgress()
End Sub
Private Sub HandleProcessCompleted(ByVal sender As Object, ByVal e As ProcessCompletedEventArgs)
Finish()
End Sub
Obviously there's a lot more footwork there. In each of the event handlers, it's safe to do things to controls.
Now, if you're using Thread instances, or ThreadPool, or manipulating tasks without Async/Await, there's more tricks. Every control has a property named InvokeRequired that is safe to get from any thread. If the code that gets it is running on the "wrong" thread, it will return True.
Every control also has an Invoke() method, that takes a delegate. (Delegates are variables that can store method calls.) It will make sure the UI thread calls that delegate the next time it's not busy. You can combine this with InvokeRequired to add methods to your form that are safe to call from any thread. For example, here's one that updates a TextBox:
Code:
Public Sub UpdateStatus(ByVal statusMessage As String)
If txtStatus.InvokeRequired Then
txtStatus.Invoke(Sub() UpdateStatus(statusMessage))
Else
txtStatus.Text = statusMessage
End If
End Sub
That's safe to call from anywhere, because it checks if it's on the right thread before doing anything. This is a really common pattern because it's as old as .NET. (That doesn't mean it's bad.) In WPF, you do something slightly different with an object called the Dispatcher. In ASP .NET, I'm pretty sure there's a different mechanism. This isn't the first trick I reach for, but there are times when the other techniques can't do what you need so it's nice to know this one, too.
So, what do you need to do?
You need to go over all of your code and find the places where you use a control on the wrong thread. Then, you need to consider ways to make that access safe. Sometimes the answer is, "Ah, I can do this after the task completes." Other times it's, "No, I need to do this right now, I need to use Invoke()." I suggest taking it very slow, changing one thing at a time and making sure it works before moving to another. If you get stuck, post the code involved. Lots of us have spent lots of time handling these issues, it's one of the things I think gets the fastest and best answers on this forum.
It's also my hobby topic. I /love/ writing about async code.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
-
Feb 17th, 2016, 11:54 AM
#5
Thread Starter
Lively Member
Re: Struggling with Form multithreading
Sitten
Thank you very much for your very in-depth response. So far I've simply skimmed it, and my next plan is to take it away and give it the undivided attention it deserves.
One or two things I wanted to make you aware of though with regards to my application. In your last paragraph you say that I need to go through the code any find all the places where I use a control in the wrong thread. Correct me if I'm wrong, but this suggests to me that there would be controls such as buttons or text boxes that the user might wish to interact with while the form is doing something else. If I'm correct in saying this then I can tell you that there are no additional user-interactive controls on the form. My sole purpose here is to give my users a program that they can use to carry out an everyday business function. There are multiple buttons that the users can press, but I'm not expecting them to be able to click any buttons while the program is performing a task.
To put you in the picture, the existing function is handled almost entirely by Microsoft Access and is very prone to crashing (as well as Access' many other foibles). So, it isn't that I'm looking to have the program respond to multiple user "requests" at the same time. Put simply, all I want to be able to do is set up a program which give the user as much feedback as possible on what it is currently doing. That feedback might be in the form of an incrementing progress bar, or perhaps a series of messages scrolling down through a status window. I'm not at all concerned if the UI appears to be frozen while the process is being run, as long as it's displaying some sort of regular message which tells the user "Hey, don't hassle me, I'm busy here!".
The code that I've got set up so far has this behind the appropriate button:
Code:
If Not m_ThreadList(0) Is Nothing Then
If Not CType(m_ThreadList(0), Thread).IsAlive Then threadStart(0)
Else
threadStart(0)
End If
Exit Sub
The threadStart subroutine looks like this:
Code:
Private Sub threadStart(ByVal threadID As Integer)
Me.KeyPreview = False
Dim objThreadClass As New clsThread(threadID, Me)
Dim objNewThread As New Thread(AddressOf objThreadClass.StartThread)
objNewThread.IsBackground = True
objNewThread.Start()
Select Case threadID
Case 0
m_ThreadList.Item(0) = objNewThread
Case 1
m_ThreadList.Item(1) = objNewThread
End Select
End Sub
The good stuff that I need to have working asynchronously is a lot of code in a subroutine, so I'm not going to place it here. Suffice it to say, it makes a number of calls to other class files as well as to other functions and subroutines contained within the form class. Essentially I don't want to have any "clever" code behind my UI, but separate. If you want to see the code as it sits in its entirety, I'd prefer to send you the project by email.
Thanks
-
Feb 17th, 2016, 12:13 PM
#6
Re: Struggling with Form multithreading
I didn't mean user interaction. One detail I forgot but will remember for the next time I type it all out:
Anything the user does with the keyboard and mouse (or a touchscreen) will only ever happen on the UI thread. So anything in a control's event handler is intrinsically safe to work with that control, because you won't ever get a button's Click event from a non-UI thread.
What I meant by "doing something with a control" is this:
Code:
Sub OnAnotherThread()
Dim userInput = txtInput.Text ' WRONG.
DoSomeThings(userInput)
txtOutput.Text = _results ' WRONG.
End Sub
Getting a property is "doing something". Setting a property is "doing something". Calling a Sub is "doing something". The only safe property from all threads is InvokeRequired. The only safe method from all threads is Invoke(). Anything else is wrong, unless you can prove it is being done from the UI thread.
I don't really accept projects by email. In general, for this kind of thing, it is best to create a separate example with just the code you're having trouble with. This is hard to do when the project involves a lot of large pieces. That's part of why I advocate small pieces: that makes surgery easy. If you can't use the exact code, it's best to come up with analogous processes, like:
I have a button that, when clicked, disables itself and starts a thread. The thread's job is to open a file, extract a lot of data, update the file, then put some results in the UI. It should update a progress bar. After the task is finished, the button should be enabled again.
I can write code that follows that process with imaginary classes. Then, you can study how that is put together and figure out how to bolt it into your project. The smaller and more clear that description is, the easier the example will be to understand.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
-
Feb 17th, 2016, 12:47 PM
#7
Thread Starter
Lively Member
Re: Struggling with Form multithreading
Ah, I think I now see what you're driving at. I think the best thing I can do now is to follow your advice:
 Originally Posted by Sitten Spynne
In general, for this kind of thing, it is best to create a separate example with just the code you're having trouble with. This is hard to do when the project involves a lot of large pieces. That's part of why I advocate small pieces: that makes surgery easy.
Tags for this Thread
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|