Background:
I have a program that is processing lots of database records, and generating tasks to do. (In this case creating user accounts in AD).
Part of this is to create the user directories, for profiles and home directories, and setting the permissions on them.
This needs to wait until the ad account has replicated across all of our DC's.
So, my program will have a separate thread responsible for creating the directories, that will process a queue populated from the main thread.
I've done some research on Threading and come up with the following code pattern:
Imports System.Threading
Public Class Form1
Dim worker As Object
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
worker = New workerObj(AddressOf resultcallback)
Dim t As New Thread(AddressOf worker.mainloop)
End Sub
Public Sub resultcallback(ByVal item As String)
Outbox.AppendText(item)
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
worker.addItem(inbox.Text)
End Sub
End Class
Public Delegate Sub resultcallback(ByVal item As String)
Public Class workerObj
Private myQueue As New Queue(Of String)
Private myCallback As resultcallback
Dim item As String = "nout"
Public Sub New(ByVal callbackdelegate As resultcallback)
myCallback = callbackdelegate
End Sub
Public Sub mainloop()
While True
If myQueue.Count > 0 Then
item = myQueue.Dequeue()
myCallBack(item)
End If
Thread.Sleep(5000)
End While
End Sub
Public Sub addItem(ByVal item As String)
myQueue.Enqueue(item)
End Sub
End Class
Problem:
On the line Dim t as new Thread.....
Error 1 Overload resolution failed because no accessible 'New' is most specific for these arguments:
'Public Sub New(start As System.Threading.ParameterizedThreadStart)': Not most specific.
'Public Sub New(start As System.Threading.ThreadStart)': Not most specific. n:\visual studio 2013\Projects\ThreadTest\ThreadTest\Form1.vb 7 13 ThreadTest
Can anyone help tell me where I have gone wrong?
Cheers.
Threads do not have a public constructor, you need to call Thread.Start. I'd suggest you don't do that though. Writing thread-safe code is tricky enough when you do know about multithreaded programming.
Eg in your code you modify a Queue from two different threads without locking. Queue isn't thread safe and you can corrupt the queue. You should lock access to it or use ConcurrentQueue which is thread-safe. Another error is trying to modify a TextBox from another thread - this will lead to an Exception because only the UI thread is allowed to modify UI controls.
A better option though is to use the ActionBlock class from the DataFlow library which already does what you want: queue requests and process them in one or more separate threads.
Your code can be as simple as this:
Dim myFileWorker=New ActionBlock(Of string)(Function(path) =>DoSomething(path))
For Each somePath As String in ListWithManyPaths
myFileWorker.Post(somePath)
Next somePath
myFileWorker.Complete()
myFileWorker.Completion.Wait()
By default only one path will be processed at a time. To process multiple paths you pass an ExecutionDataflowBlockOptions object with the desired MaxDegreeOfParallelism:
Dim options=New ExecutionDataflowBlockOptions() With { .MaxDegreeOfParallelism=5}
Dim myFileWorker=New ActionBlock(Of String) Function(path) DoSomething(path),options)
Related
my problem is that in a WCF service (on a network call) the thread doesn't come back. Unfortunately, I don't get any return values for functions or code after the join method is not executed.
The WCF service itself has a web interface that creates a thread because a form with TreeView and AllowDrop activated there would later return an error:
System.InvalidOperationException: "DragDrop registration failed"
ThreadStateException: STA mode (Single Thread Apartment) must be set for the current thread before OLE calls can be made. Make sure that the main function is marked with STAThreadAttribute.
If, on the other hand, I start a sample project "WindowsApp56" (no WCF service), Form1 is opened by ClassLibrary7 and code according to the join method is activated. The message box "ready" is now displayed.
No errors are thrown with the web service, when the thread isn't coming back.
Project WindowsApp56:
Imports System.Threading
Imports ClassLibrary7
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim tmp As New Class1
Dim myobject As System.Threading.Thread = New Thread(New ThreadStart(AddressOf tmp.Main))
myobject.TrySetApartmentState(ApartmentState.STA)
myobject.Name = "Tester1"
myobject.Start()
If myobject.ThreadState <> ThreadState.Unstarted Then
myobject.Join()
End If
MsgBox("ready")
End Sub
End Class
ClassLibrary7:
Public Class Class1
Public Sub Main()
Dim a As New Form1
a.ShowDialog()
End Sub
End Class
Is the thread join method wrong? How can I "wait for" return values.
According to the error message: 'STA mode (Single Thread Apartment) must be set for the current thread before OLE calls can be made. Make sure that the main function is marked with STAThreadAttribute.'
Add the STAThreadAttribute attribute on the Main method.
Public Class Class1
<STAThread()>
Public Sub Main()
Dim a As New Form1
a.ShowDialog()
End Sub
End Class
I would like to be able to change an element of the main form inside a thread declared in a separated class (In this case I want to change a label text).
I tried the following code:
Form1:
Imports System.Threading
Public Class Form1
Public counter As Integer = 0
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim SecondClassObject As New SecondClass()
End Sub
End Class
SecondClass:
Imports System.Threading
Public Class SecondClass
Public Thread As New Thread(AddressOf Increment)
Public counter As Integer = 0
Sub New()
Thread.Start()
End Sub
Sub Increment()
While True
Form1.Label1.Text = counter
counter += 1
End While
End Sub
End Class
If I do the same thing using a thread but in the form code itself than the label text will change:
Imports System.Threading
Public Class Form1
Public counter As Integer = 0
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim thread As New Thread(AddressOf Increment)
thread.Start()
End Sub
Sub Increment()
While True
Label1.Text = counter
counter += 1
End While
End Sub
End Class
How should I do in order to archieve the same result using a thread in a separated class?
First, I do want to point out that you need to use an Invoke/Callback to safely set the label's text from the secondary thread. I don't know if you're doing that in your actual code base, but wanted to specify anyways.
Now, focused on the actual question, I believe that the easiest way to do as requested is to pass a reference to the original instance of Form1 to your SecondClass. Having a reference to the parent, means that you would be able to manipulate the parent's publicly exposed elements as needed.
Consider the below:
Public Class Form1
Public counter As Integer = 0
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim secondClass As New SecondClassObject(me)
End Sub
Delegate Sub SetTextCallback (value as String)
Public Sub SetText (value as string)
if me.Label1.InvokeRequired Then
dim d as New SetTextCallback(addressOf SetText)
Me.Invoke(d, New Object() {value})
Else
me.label1.text = value
End If
End Sub
End Class
Public Class SecondClassObject
private _parent as Form1
private myThread As New Thread(AddressOf Increment)
Public Sub New (byref p as Form1)
me._parent = p
myThread.Start()
End Sub
Sub Increment()
While True
Me._parent.SetText(counter)
counter += 1
End While
End Sub
End Class
What is happening is that the a reference to the parent is passed into the second class as a constructor, doing so allows us to interact with the parent from the second class.
Now, that is one way, but other options do exist. Things such as specialized events/handlers or wiring up databinding between the Form1.Label1 and a property exposed from the SecondClassObject. Even a singleton pattern, where the value to be incremented is shared between all instances, so when the SecondClassObject increments it, Form1 would be aware and know to update Label1.
Also, please note that the above code is for example purposes, and is missing things such as a defined declaration for Label1.
On windows it's not possible to change the UI from a non UI thread.
It looks like that you have to use Control.Invoke or better Control.BeginInvoke.
The problem with using Control.Invoke is that it's executed on the UI thread and the calling thread waits for completion. Which would be bad when your background worker continusly does some computations.
Hoping to get some best-practice advise with regards to capturing a returned message from an instantiated class on my form.
In my form (form1.vb), I have a label which reflects what is being done, with the code below.
Code in form1.vb to display message:
Public Sub DisplayMessage(ByVal Msg as String, ByVal Show as Boolean)
Application.DoEvents()
If Show Then
lblShow.Text = Msg
lblShow.Refresh()
End If
End Sub
I have came across three methods so far:
Direct Form Call. In this scenario the class directly calls the form's message routine:
form1.DisplayMessage("Show This Message", True)
RaiseEvent within class. In this scenario form1 is Friends WithEvents of the class sending the message, and the class raises the event to the form.
**Declared in Form1.vb**
Friend WithEvents Class1 as New Class1
**Declared in Class1.vb**
Public Event SetMessage(ByVal Msg As String, ByVal Show As Boolean)
**Used in Class1.vb**
RaiseEvent SetMessage("Show This Message", True)
Have an EventArgs class handle the event. In this scenario we have an EventArg.vb class which is instantiated whenever we raise the event.
**Declared in Form1.vb**
Friend WithEvents Class1 as New Class1
Private Sub class1_DisplayMessage(ByVal Msg As String, ByVal showAs Boolean, ByRef e As ProgressMessageEventArgs) Handles Class1.SetMessage
DisplayMessage(Msg, Show)
End Sub
**Declared in Class1.vb**
Public Event SetMessage(ByVal msg As String, ByVal Show As Boolean, ByRef e As ProgressMessageEventArgs)
Protected Sub CaptureMessage(ByVal msg As String, ByVal Show As Boolean)
RaiseEvent SetMessage(message, ShowList, New ProgressMessageEventArgs(message))
End Sub
**Used in Class1.vb**
RaiseEvent CaptureMessage("Show This Message", True)
**EventArg.vb created to handle ProgressMessageEventArgs class**
Public NotInheritable Class ProgressMessageEventArgs
Inherits System.EventArgs
Public txt As String
Public Sub New(ByVal txt As String)
MyBase.New()
Me.Text = txt
End Sub
End Class
Scenario 1 is seemingly the simplest, though I was advised against this and asked to raise an event instead. Over time I came across scenario 3 which involves an additional class vs scenario 2.
Therefore, the question is...
Between these three methods, which would be the "proper" way of returning a message from a class to the form? Is the additional EventArg class as per scenario 3 necessary since scenario 2 works fine as well?
Many thanks in advance.
My answer is none of the above. Consider this example
Public Class Form1
Private WithEvents myClass1 As New Class1()
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
myClass1.CountTo1000()
End Sub
Private Sub MyClass1_Updated(number As Integer) Handles myClass1.Updated
Me.Label1.Text = number.ToString()
End Sub
End Class
Public Class Class1
Public Event Updated(number As Integer)
Public Sub CountTo1000()
For i = 1 To 1000
System.Threading.Thread.Sleep(1)
RaiseEvent Updated(i)
Next
End Sub
End Class
You have a form and a class, and the form has a reference to the class (the class doesn't even know the form exists). Your business logic is performed in the class, and the form is used to input and display information. CountTo1000() is being called directly from the form, which is bad because basically the UI thread is being put to sleep 1000 times, while the class is trying to update the UI by raising the event after each sleep. But the UI never has time to allow the events to happen, i.e. to be updated. Placing an Application.DoEvents() after Me.Label1.Text = number.ToString() will allow the UI to update. But this is a symptom of bad design. Don't do that.
Here is another example with multi-threading
Public Class Form1
Private WithEvents myClass1 As New Class1()
' this handler runs on UI thread
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' make a new thread which executes CountTo1000
Dim t As New System.Threading.Thread(AddressOf myClass1.CountTo1000)
' thread goes off to do its own thing while the UI thread continues
t.Start()
End Sub
' handle the event
Private Sub MyClass1_Updated(number As Integer) Handles myClass1.Updated
updateLabel(number.ToString())
End Sub
' invoke on UI thread if required
Private Sub updateLabel(message As String)
If Me.Label1.InvokeRequired Then
Me.Label1.Invoke(New Action(Of String)(AddressOf updateLabel), message)
Else
Me.Label1.Text = message
End If
End Sub
End Class
Public Class Class1
Public Event Updated(number As Integer)
Public Sub CountTo1000()
For i = 1 To 1000
System.Threading.Thread.Sleep(1)
RaiseEvent Updated(i)
Next
End Sub
End Class
This simple example shows how a thread can be created and run some code off the UI. When doing this, any method call from the non-UI thread must be invoked on the UI if it must access a UI control (Label1). The program runs smoothly since the Thread.Sleep is done on a different thread than the UI thread, with no need for Application.DoEvents, because the UI thread is otherwise doing nothing, and can handle the events being raised by the other thread.
I focused more on threading, but in both examples the design has a form with a class, and the form knows about the class, but the class doesn't know about the form. More about that can be seen here.
See also:
Why we need to check for InvokeRequired, then invoke: Control.InvokeRequired
A better option than Thread nowadays: BackgroundWorker
An even cooler option, if you can wrap your head around it: Async/Await
In order to keep responsiveness in the UI, I use a separate thread to execute various process, for example some FTP download.
Private Sub Button11_Click(sender As Object, e As EventArgs) Handles Button11.Click
Dim ThreadResync As System.Threading.Thread
ThreadResync = New System.Threading.Thread(AddressOf Bodacc_ResyncFTP)
ThreadResync.Start()
End Sub
Sub Bodacc_ResyncFTP()
Dim MyBodacc As bodacc_data = New bodacc_data
MyBodacc.Label_Status = Form1.Label1
MyBodacc.ResyncFTP()
End Sub
A way to update the UI with threading is the Delegate thingy, so in the bodacc_data I had to
Public Class bodacc_data
......
Delegate Sub UpdateLabelDelg(text As String, ThisLabel As Label)
Public Delegate_label As UpdateLabelDelg = New UpdateLabelDelg(AddressOf set_label)
Public Label_Status = Label
......
Sub set_label(stext As String, ThisLabel As Label)
ThisLabel.Text = stext
End Sub
.....
Sub ResyncFTP()
//says hello
If Label_Status.InvokeRequired = True Then
Label_Status.Invoke(Delegate_label, New Object() {"Working...", Label_Status})
Else
Label_Status.Text = "Working..."
End If
//do stuff
End Sub
End Class
It works like a charm. But I have many class doing more or less the same (disk update, database update, FTP update) and having to copy/past all the delegate / external label declaration / mini sub / invoke sound silly.
So I created a class to handle those UI update / delegate in order to have a quick access
Public Class Form_UI
Delegate Sub UpdateLabelDelg(text As String, ThisLabel As Label)
Public Delegate_label As UpdateLabelDelg = New UpdateLabelDelg(AddressOf set_label)
Private Labels(2) As Label
Sub New()
Labels(0) = Form1.Label1
Labels(1) = Form1.Label2
Labels(2) = Form1.Label3
End Sub
Sub set_label(stext As String, ThisLabel As Label)
ThisLabel.Text = stext
End Sub
Public Sub ChangeLabel(ByVal LabelNum As Integer, nText As String)
LabelNum = LabelNum - 1
If Labels(LabelNum).InvokeRequired Then
Labels(LabelNum).Invoke(Delegate_label, New Object() {nText, Labels(LabelNum)})
Else
Labels(LabelNum).Text = nText
Labels(LabelNum).Update()
End If
End Sub
End Class
So, now in the revamped bodacc_data and all others processing class I have only :
Public Class bodacc_data
......
Private MyUI as Form_UI
.....
Sub New()
MyUI = New Form_UI()
End Sub
Sub ResyncFTP()
//says hello
MyUI.ChangeLabel(1, "Working...")
//do stuff
End Sub
End Class
Question Why is MyUI.ChangeLabel not updating when the ResyncFTP is called in a thread, but works if called in the main thread (As in the code sample below)
Private Sub Button11_Click(sender As Object, e As EventArgs) Handles Button11.Click
Dim MyBodacc As bodacc_data = New bodacc_data
MyBodacc.ResyncFTP()
End Sub
Note that there is no error thrown. The notable weirdness is that <Form_UI>.ChangeLabel() never goes the .Invoke route but the normal update route. I strongly suspect a scope issue or insight issue.
When you create a windows forms app you set up a UI thread that is meant to be the owner of all the UI. The UI thread contains the message pump that is used to update all of the UI.
But what you're doing in Button11_Click is creating a new thread that goes and calls Dim MyBodacc As bodacc_data = New bodacc_data which, in turn, calls MyUI = New Form_UI().
You are creating a form on a non-UI thread. There is no message pump and therefore the UI doesn't update.
Form1.vb
Imports System.Threading
Public Class Form1
Dim demoThread As Thread
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim Start As New Class1
Me.demoThread = New Thread( _
New ThreadStart(AddressOf Start.ThreadProcSafe))
Me.demoThread.Start()
End Sub
Delegate Sub SetTextCallback([text] As String)
Public Sub SetText(ByVal [text] As String)
' InvokeRequired required compares the thread ID of the
' calling thread to the thread ID of the creating thread.
' If these threads are different, it returns true.
If Me.textBox1.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, New Object() {[text]})
Else
Me.textBox1.Text = [text]
End If
End Sub
End Class
Class1.vb
Public Class Class1
Public Sub ThreadProcSafe()
Form1.SetText("This text was set safely.")
End Sub
End Class
Can someone tell me why this doesn't update the textbox?
It works when ThreadProcSafe is called when its inside Form1(and is still started by a thread) but when it's moved outside of the class into another, no warnings or errors but doesn't update.
The reason is that you are referring to the default instance in your second code snippet. Default instances are thread-specific so that second code snippet will create a new instance of the Form1 type rather then use the existing instance. Your Class1 needs a reference to the original instance of Form1.
If that's not practical then the solution is to not do the delegation in the form but rather do it in the class accessing the form, using the SynchronizationContext class.