Writing a multi-threaded class
For this we are going to use the same count sub but lets make things a little more interesting by changing it into a function that returns a value after its finished.
vbnet Code:
Public Class Counter
Public Function Count(ByVal Max As Integer) As String
Dim startTime As DateTime = DateTime.Now
For i = 1 To Max
Threading.Thread.Sleep(200)
Next
Return "Count took : " + (DateTime.Now - startTime).ToString
End Function
End Class
Our Count sub is now a function that returns a String that states the amount of time it took to complete.
Now start a new Windows Forms project and add a new code file and within it place the above code. On the Form place a Label and a Button and code the Button's Click event handler as:-
vbnet Code:
'
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim cn As New Counter
Label1.Text = cn.Count(20)
End Sub
Run the program and click the button. You'd notice, just like the start of our previous project in post #2, you can't do anything until the count is finished. You can't move the form or click anything on it because the UI thread is currently executing the Count function so it can't process any mouse or keyboard messages until it is free from Count. When Count is finished, it returns a value which we display in the label.
Before applying multi-threading to the class I want to address a limitation of functions like the Start method of the Thread class and QueueUserWorkItem of the ThreadPool class. These methods have a rather annoying limitation of only being able to take the address of functions with only one parameter and Option Strict On would demand this parameter be of the Object type. What if Count had more than one parameter ? We would have to write an overload to take one Object parameter and have that overload call the real Count, getting the parameter from an object made to hold the parameters as fields or properties. If you were to have other multi-threaded methods in the class then you would have to make a class a for the arguments of each method, and a method overload for each to take a single Object parameter made of one of these classes. In other words you have to create two extra entities(a method and a class) for every method you want to call on another thread. This is really retarded, there is just no excuse for that. MS should and could have made Start/QueueUserWorkItem more flexible. Thankfully I came up with a one-size fits all solution. One MS could have easily come up with.
Outside of the Counter class add this class:-
vbnet Code:
'
Public Class ThreadExtensions
Private args() As Object
Private DelegateToInvoke As [Delegate]
Public Shared Function QueueUserWorkItem(ByVal method As [Delegate], ByVal ParamArray args() As Object) As Boolean
Return Threading.ThreadPool.QueueUserWorkItem(AddressOf ProperDelegate, New ThreadExtensions With {.args = args, .DelegateToInvoke = method})
End Function
Private Shared Sub ProperDelegate(ByVal state As Object)
Dim sd As ThreadExtensions = DirectCast(state, ThreadExtensions)
sd.DelegateToInvoke.DynamicInvoke(sd.args)
End Sub
End Class
I'm not going to go into the details of how that works because I'll have to explain way too much that has nothing to do with multi-threading at all. I'll just say what it does. Its simply a version of QueueUserWorkItem that can take the address of any sub or function and an arbitrary amount of arguments using ParamArray so we can call any method with it.
Now going back to our counter class I'd start by adding this function:-
vbnet Code:
'
Public Sub CountAsync(ByVal Max As Integer)
ThreadExtensions.QueueUserWorkItem(New Func(Of Integer, String)(AddressOf Count), Max)
End Sub
Its a kin function to Count that calls Count on a thread pool thread using our special QueueUserWorkItem. Now our class has two ways to call Count. One is synchronous and one is asynchronous. Notice that we declare the asynchronous one as a sub because it would return immediately after its called hence we cannot get a return value because the count would be taking place on another thread while the calling thread would proceed as normal.
The next thing we want to do is to create a private field in our Counter class:-
vbnet Code:
Private context As Threading.SynchronizationContext = Threading.SynchronizationContext.Current
Think of this object as an anchor into the UI thread. Our asynchronous Count would be running on another thread but remember we may want to report progress back to the UI thread by updating labels or progress bars. As demonstrated in post #2, we can't simply alter a control from any thread other than the UI thread so we need this object to submit methods to be executed on the UI thread. The SynchronizationContext class has a Send method which takes the address of the function we wish to invoke on the UI thread. Send however, suffers from the same limitation like QueueUserWorkItem, it can only the address of a function with only one parameter. We address this in the same manner as we did before:-
vbnet Code:
'
Public Shared Sub ScSend(ByVal sc As Threading.SynchronizationContext, ByVal del As [Delegate], ByVal ParamArray args() As Object)
sc.Send(New Threading.SendOrPostCallback(AddressOf ProperDelegate), New ThreadExtensions With {.args = args, .DelegateToInvoke = del})
End Sub
Add the above shared method to the ThreadExtensions class. We will use the above method to execute methods on the UI thread so we can alter controls in the proper thread safe manner.
Ok....so we're practically set. Now we need to set up a way to report the progress of the count and return the function's value. In the earlier example in post #2, we used a function to set the Label's text to reflect a where the count has reached. Now we don't want to have to write a separate function for every control we may wish to alter so a far more agreeable solution would be to raise some kind of event on the UI thread and then we are free to do anything to any control and all controls in the normal way. No need to call InvokeRequired to check for thread safety anymore. We pass of that responsibility to our Counter class.
Now, lets get to coding the events. In this I tend to stick to the patterns of setting up events that is used by MS in the .Net Framework. So we start by creating an EventArgs based object:-
vbnet Code:
'
Public Class CountChangedEventArgs
Inherits EventArgs
Private _CurrentCount As Integer
Private _Max As Integer
Public Sub New(ByVal cc As Integer, ByVal max As Integer)
_CurrentCount = cc
_Max = max
End Sub
Public ReadOnly Property CurrentCount() As Integer
Get
Return _CurrentCount
End Get
End Property
Public ReadOnly Property Max() As Integer
Get
Return _Max
End Get
End Property
End Class
Place the above class outside of the Counter class.
Now add these lines of code to the Counter class:-
vbnet Code:
'
Public Event CountChanged As EventHandler(Of CountChangedEventArgs)
Protected Overridable Sub OnCountChanged(ByVal e As CountChangedEventArgs)
RaiseEvent CountChanged(Me, e)
End Sub
Our Counter class now has a CountChanged event which we would now raise. Remember, we should raise the event on the UI thread so any code in the event handler would be free to alter controls without worrying about cross-thread calls:-
vbnet Code:
'
Public Function Count(ByVal Max As Integer) As String
Dim startTime As DateTime = DateTime.Now
Dim e As CountChangedEventArgs
For i = 1 To Max
e = New CountChangedEventArgs(i, Max)
If context Is Nothing Then
OnCountChanged(e)
Else
ThreadExtensions.ScSend(context, New Action(Of CountChangedEventArgs)(AddressOf OnCountChanged), e)
End If
Threading.Thread.Sleep(200)
Next
Return "Count took : " + (DateTime.Now - startTime).ToString
End Function
Change the Count sub to the above. We use the ThreadExtensions function we wrote earlier to have the SynchronizationContext object raise the event on the UI thread for us in the case context is not Nothing. If context is Nothing we simply raise the event in the normal way. I do not understand the SynchronizationContext enough to tell you the circumstances under which it would be Nothing but in the most common scenarios it should have a proper object.
The only thing remaining now is that return value. We simply use another event. Like before we create a new EventArgs object:-
vbnet Code:
'
Public Class CountCompletedEventArgs
Inherits EventArgs
Private Dim _message As String
Public Sub New(ByVal msg As String)
_message = msg
End Sub
Public ReadOnly Property Message() As String
Get
Return _message
End Get
End Property
End Class
And add the following to the Counter class:-
vbnet Code:
'
Public Event CountCompleted As EventHandler(Of CountCompletedEventArgs)
Protected Overridable Sub OnCountCompleted(ByVal e As CountCompletedEventArgs)
RaiseEvent CountCompleted(Me, e)
End Sub
Now we change our Count function yet again:-
vbnet Code:
'
Public Function Count(ByVal Max As Integer) As String
Dim startTime As DateTime = DateTime.Now
Dim e As CountChangedEventArgs
Dim msg As String
For i = 1 To Max
e = New CountChangedEventArgs(i, Max)
If context Is Nothing Then
OnCountChanged(e)
Else
ThreadExtensions.ScSend(context, New Action(Of CountChangedEventArgs)(AddressOf OnCountChanged), e)
End If
Threading.Thread.Sleep(200)
Next
msg = "Count took : " + (DateTime.Now - startTime).ToString
If context Is Nothing Then
OnCountCompleted(New CountCompletedEventArgs(msg))
Else
ThreadExtensions.ScSend(context, New Action(Of CountCompletedEventArgs)(AddressOf OnCountCompleted), New CountCompletedEventArgs(msg))
End If
Return msg
End Function
Our Count function now raises a completed event and passes the return value using the EventArgs object. Our Counter class in now has multi-threaded capabilities.
Now go back to the Form's code and change it to this:-
vbnet Code:
Public Class Form1
Private WithEvents m_cn As New Counter
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
m_cn.CountAsync(100)
End Sub
Private Sub m_cn_CountChanged(ByVal sender As Object, ByVal e As CountChangedEventArgs) Handles m_cn.CountChanged
Label1.Text = CStr(e.CurrentCount)
End Sub
Private Sub m_cn_CountCompleted(ByVal sender As Object, ByVal e As CountCompletedEventArgs) Handles m_cn.CountCompleted
MsgBox(e.Message)
End Sub
End Class
The Form must have a Label and a Button. Run the application and click the button and you should see the count progressing indicated by the label and a message box pop up when the count is done, showing the return value which is the time the count took to complete. So there you have it. A working multi-threaded class. There is a little more I'd like to write on this subject but I'm too near the 15K character limit for this post so I'll have to cut it here. Please feel free to post any questions you may have in the thread. Good luck