Generic Wrapper for Easy Multithreaded Programming
Event-based, generic wrapper and manager to implement multithreading in your applications
Implementing multithreading in your applications is not always an easy task. For relatively simple solutions, the BackgroundWorker
component present in the .NET Framework since version 2.0 provides a straightforward answer.
However, for more sophisticated asynchronous applications, Microsoft suggests implementing a class that adheres to the Event-based Asynchronous Pattern. This is cool, but still a little hard to do, and you need to repeat it all over again each time you need to run some stuff in a different thread.
I've done a few experiments myself with multithreading, starting with .NET version 1.1. Although I wouldn't call myself an expert on the subject, I'm sure of a few things:
- It's not easy
- It's hard to debug
- Organizations should not allow junior developers to start coding multithreaded applications without careful thinking and without the help of an experienced developer (one who knows about multithreading)
To help reduce these concerns, I've always wondered how I could make multithreading easier by somehow wrapping and handling some of its inherent complexity inside reusable base classes and helpers. This way, the developer would be able to concentrate less on the technical aspects coming with multithreading, and more on the features she/he wants to implement as multithreaded, and how they will interact with the main thread.
What I wanted my wrappers and manager classes to do was:
- Handle the creation of new threads
- Handle exceptions occurring in the created threads
- Easily switch between running the workers asynchronously and synchronously (first to see the impact on performance, and secondly to help debugging – although I agree it's not perfect)
- Handle inter-thread communication between parent and child threads (progression report, reporting results at the end of the worker thread, interruption requests, etc.)
With Generics and the use of the AsyncOperationManager
and AsyncOperation
classes, this has become really possible, and in my opinion, pretty clean.
Audience
In this article, I will not describe or explain the concepts behind threading, how they work, or how to use them. The article is not a reference guide to every possible aspect and technicality related to threading. So, it mostly targets developers and architects who are already aware of those details. You need to understand various threading principles, like locking mechanisms when sharing objects between threads and how to make these classes thread-safe, using threads to update the UI, etc.
If you would like to learn about those, there is a very good article written by Sacha Barber that introduces all of those concepts. You can find it here: Beginners Guide to Threading in .NET Part 1 of n.
Source Code and Demo
I have included the source code with this article, along with a demo project which uses three threads at the same time (plus the main UI thread), with the third thread being managed and started by the second one.
You can download the source code from the link above.
Class Diagrams
Here are the class diagrams for the wrapper/helper library.
Event Argument Classes
OK, let's look at the code now. First, I am publishing two different events from my manager, so let's review the event argument classes for them.
Namespace Events
#Region "Class BaseEventArgs"
''' (summary)
''' Abstract base class for all defined events
''' (/summary)
''' (remarks)(/remarks)
Public MustInherit Class BaseEventArgs
Inherits System.EventArgs
''' (summary)
''' ID of the worker that fires the event
''' (/summary)
''' (remarks)(/remarks)
Private aIdentity As String
''' (summary)
''' ID of the worker that fires the event
''' (/summary)
''' (value)aIdentity (String)(/value)
''' (returns)Identity of the worker that fires the event(/returns)
''' (remarks)(/remarks)
Public ReadOnly Property Identity() As String
Get
Return aIdentity
End Get
End Property
''' (summary)
''' Base constructor
''' (/summary)
''' (param name="identity")Identity of the worker that fires the event(/param)
''' (remarks)(/remarks)
Protected Sub New(ByVal identity As String)
aIdentity = identity
End Sub
End Class
#End Region
#Region "Class WorkerDoneEventArgs"
''' (summary)
''' Arguments for the event that signals the end of the worker
''' (/summary)
''' (typeparam name="TResultType")
''' Type of the object that will contain the results of the worker
''' (/typeparam)
''' (remarks)(/remarks)
Public Class WorkerDoneEventArgs(Of TResultType)
Inherits BaseEventArgs
''' (summary)
''' Worker result
''' (/summary)
''' (remarks)(/remarks)
Private aResult As TResultType
''' (summary)
''' Possible exception that has occurred in the worker
''' (/summary)
''' (remarks)(/remarks)
Private aException As Exception
''' (summary)
''' Flags that indicates if the worker has received an interruption request
''' (/summary)
''' (remarks)(/remarks)
Private aInterrupted As Boolean
''' (summary)
''' Worker result
''' (/summary)
''' (value)aResult (TResultType)(/value)
''' (returns)The worker results, or Nothing if an exception occurred(/returns)
''' (remarks)(/remarks)
Public ReadOnly Property Result() As TResultType
Get
Return aResult
End Get
End Property
''' (summary)
''' Possible exception that has occurred in the worker
''' (/summary)
''' (value)aException(/value)
''' (returns)Nothing if there was not exception, otherwise the exception
''' (/returns)
''' (remarks)(/remarks)
Public ReadOnly Property Exception() As Exception
Get
Return aException
End Get
End Property
''' (summary)
''' Flags that indicates if the worker has received an interruption request
''' (/summary)
''' (value)aInterrupted (Boolean)(/value)
''' (returns)True if the worker has received an interruption request,
''' otherwise False(/returns)
''' (remarks)(/remarks)
Public ReadOnly Property Interrupted() As Boolean
Get
Return aInterrupted
End Get
End Property
''' (summary)
''' Constructor
''' (/summary)
''' (param name="identity")Identify of the worker(/param)
''' (param name="result")Worker result(/param)
''' (param name="exception")Exception (if any) that occurred in the worker
''' (/param)
''' (param name="interrupted")Indicates if the worker
''' received an interruption request(/param)
''' (remarks)(/remarks)
Public Sub New(ByVal identity As String, _
ByVal result As TResultType, _
ByVal exception As Exception, _
ByVal interrupted As Boolean)
MyBase.New(identity)
If Not result Is Nothing Then
aResult = result
End If
If Not exception Is Nothing Then
aException = exception
End If
aInterrupted = interrupted
End Sub
End Class
#End Region
#Region "Class WorkerProgressEventArgs"
''' (summary)
''' Arguments for the event that signals a progression in the worker
''' (/summary)
''' (typeparam name="TProgressType")
''' Type of the object that will contain the progress data
''' (/typeparam)
''' (remarks)(/remarks)
Public Class WorkerProgressEventArgs(Of TProgressType)
Inherits BaseEventArgs
''' (summary)
''' Progress data
''' (/summary)
''' (remarks)(/remarks)
Private aProgressData As TProgressType
''' (summary)
''' Progress data
''' (/summary)
''' (value)aProgressData (TProgressType)(/value)
''' (returns)The progress data coming from the worker(/returns)
''' (remarks)(/remarks)
Public ReadOnly Property ProgressData() As TProgressType
Get
Return aProgressData
End Get
End Property
''' (summary)
''' Constructor
''' (/summary)
''' (param name="identity")Identify of the worker(/param)
''' (param name="progressData")Progress data(/param)
''' (remarks)(/remarks)
Public Sub New(ByVal identity As String, _
ByVal progressData As TProgressType)
MyBase.New(identity)
If Not progressData Is Nothing Then
aProgressData = progressData
End If
End Sub
End Class
#End Region
End Namespace
You can see I have a base "must inherit" class (from which the other two inherit) that contains an Identity
field that will be present in both the event argument classes. The identity exists so that you can identify the work being processed by your worker thread.
The WorkerDoneEventArgs
class has a generic TResultType
, which indicates the type of object used to store the results of a worker. The class also offers a member to store an exception that can occur in the worker, and a flag Interrupted
to indicate if an interruption request was received by the worker.
The WorkerProgressEventArgs
class has a generic TProgressType
, which indicates the type of object used to store the progression data of a worker. This could be as simple as an Integer
to only give a percentage of progression, or a full-blown custom class holding other information needed by the owner of the worker, between each progression step.
Worker Interface (IWorker)
Next, I need a simple interface to define the members of the base worker class, as seen by the manager, which will make use of them.
Namespace Worker
''' (summary)
''' Worker interface (without any generics)
''' (/summary)
''' (remarks)(/remarks)
Public Interface IWorker
#Region "Properties"
''' (summary)
''' Used between AsyncManager and WorkerBase, to become AsyncManagerTyped
''' (/summary)
''' (returns)The handle to the parent async manager(/returns)
''' (remarks)(/remarks)
Property AsyncManagerObject() As Object
''' (summary)
''' Used by the derived worker class to check
''' if an interruption request was received
''' (/summary)
''' (returns)Flag that indicates
''' if an interruption request was received(/returns)
''' (remarks)(/remarks)
ReadOnly Property InterruptionRequested() As Boolean
''' (summary)
''' Used to hold the results of the worker, as object
''' (/summary)
''' (returns)Worker result(/returns)
''' (remarks)(/remarks)
ReadOnly Property ResultObject() As Object
''' (summary)
''' Used to hold the exception (if any) that occurred in the worker
''' (/summary)
''' (returns)Exception (if any) that occurred in the worker(/returns)
''' (remarks)(/remarks)
ReadOnly Property WorkerException() As Exception
#End Region
#Region "Subs and Functions"
''' (summary)
''' Called by the AsyncManager to start the worker in asynchronous mode
''' (/summary)
''' (remarks)(/remarks)
Sub StartWorkerAsynchronous()
''' (summary)
''' Called by the AsyncManager to start the worker in synchronous mode
''' (/summary)
''' (remarks)(/remarks)
Sub StartWorkerSynchronous()
''' (summary)
''' Called by the AsyncManager to stop the worker (interruption request)
''' (/summary)
''' (remarks)(/remarks)
Sub StopWorker()
#End Region
End Interface
End Namespace
First, you see the AsyncManagerObject
. It's there because the manager will need to give a handle to itself to the worker instance so that the worker can call its methods. Why is it declared as Object
? Because at this stage, I don't know the types that will be used as generics to declare an instance of the manager. You will see later in the WorkerBase
class how I transform this version of the AsyncManager
from Object
to its strongly-typed version.
The ResultObject
is similar. Since I don't know the generics used to specify the type of object for the Result
of the worker class, I have to declare it as Object
.
The interface also contains the three very obvious methods to handle the starting and stopping of the worker.
The AsyncManager Class
Next, we go to the manager class, one of the most important in the solution.
''' (summary)
''' Manager class, handling the execution of the worker
''' in synchronous or asynchronous mode
''' (/summary)
''' (typeparam name="TProgressType")
''' Type of the object that will contain the progress data
''' (/typeparam)
''' (typeparam name="TResultType")
''' Type of the object that will contain the results of the worker
''' (/typeparam)
''' (remarks)(/remarks)
Public Class AsyncManager(Of TProgressType, TResultType)
First, you see that the class uses two generic types. TProgressType
specifies the type of object used to store the progression data (which relates to WorkerProgressEventArgs
). TResultType
specifies the type of object used to store the results of the worker (which relates to WorkerDoneEventArgs
).
These generics are used throughout the manager code, to allow strong-typed signatures, preventing the consumer having to always cast every object required (which is also costly in terms of performance).
''' (summary)
''' Constructor that receives the required parameters to manage the worker's execution
''' (/summary)
''' (param name="identity")Identity of the worker(/param)
''' (param name="worker")Instance of a worker class
''' derived from Worker.WorkerBase(/param)
''' (remarks)(/remarks)
Public Sub New(ByVal identity As String, _
ByVal worker As Worker.IWorker)
aWorker = worker
aIdentity = identity
'Give a handle to ourselves to the worker
aWorker.AsyncManagerObject = Me
End Sub
The constructor receives a string
used to uniquely identify the manager and worker, and the worker instance, declared as IWorker
. You can see that the worker is given a handle to the manager, using the AsyncManagerObject
property.
''' (summary)
''' Event used to signal the end of the worker
''' (/summary)
''' (remarks)(/remarks)
Public Event WorkerDone As EventHandler(Of Events.WorkerDoneEventArgs(Of TResultType))
''' (summary)
''' Event used to signal a progression in the worker
''' (/summary)
''' (remarks)(/remarks)
Public Event WorkerProgress As EventHandler_
(Of Events.WorkerProgressEventArgs(Of TProgressType))
Next, we see the event declarations. WorkerDone
will be fired when the worker's process is finished, either normally, or after an exception has occurred, or after an interruption was received. The event arguments will allow distinction between the possible endings.
The WorkerProgress
event will only be fired when the worker's code needs it.
''' (summary)
''' Handle to the calling thread, used to call methods on the calling thread
''' (/summary)
''' (value)aCallingThreadAsyncOp(/value)
''' (returns)The AsyncOperation associated to the calling thread(/returns)
''' (remarks)(/remarks)
Friend ReadOnly Property CallingThreadAsyncOp() As AsyncOperation
Get
Return aCallingThreadAsyncOp
End Get
End Property
''' (summary)
''' Identity of the worker
''' (/summary)
''' (value)aIdentity (String)(/value)
''' (returns)Identity of the worker(/returns)
''' (remarks)(/remarks)
Public ReadOnly Property Identity() As String
Get
Return aIdentity
End Get
End Property
''' (summary)
''' Indicates if the worker is running
''' (/summary)
''' (value)Boolean(/value)
''' (returns)True if the worker is running, otherwise False(/returns)
''' (remarks)(/remarks)
Public ReadOnly Property IsWorkerBusy() As Boolean
Get
Return Not (aWorkerThread Is Nothing)
End Get
End Property
As for properties, notice CallingThreadAsyncOp() As AsyncOperation
. This is the magical object that allows communication between the worker thread and the manager thread. This object gets created through System.ComponentModel.AsyncOperationManager
, using its CreateOperation
method. You will see a little further below how we use this object to execute methods on the manager thread.
The other important property is IsWorkerBusy
, which indicates if the worker thread still exists, which means, in my design, that the worker is still busy doing its stuff.
''' (summary)
''' Used as SendOrPostCallback, called through CallingThreadAsyncOp
''' (/summary)
''' (param name="state")Object containing the parameters
''' to transfer to the strongly-typed overload(/param)
''' (remarks)(/remarks)
Friend Overloads Sub WorkerProgressInternalSignal(ByVal state As Object)
WorkerProgressInternalSignal(DirectCast(state, TProgressType))
End Sub
''' (summary)
''' Used as SendOrPostCallback, called through CallingThreadAsyncOp
''' (/summary)
''' (param name="state")Object containing the parameters
''' to transfer to the strongly-typed overload(/param)
''' (remarks)(/remarks)
Friend Overloads Sub WorkerExceptionInternalSignal(ByVal state As Object)
WorkerExceptionInternalSignal(DirectCast(state, Exception))
End Sub
''' (summary)
''' Used as SendOrPostCallback, called through CallingThreadAsyncOp
''' (/summary)
''' (param name="state")Object containing the parameters
''' to transfer to the strongly-typed overload(/param)
''' (remarks)(/remarks)
Friend Overloads Sub WorkerDoneInternalSignal(ByVal state As Object)
WorkerDoneInternalSignal(DirectCast(state, TResultType))
End Sub
To be able to call methods on the manager's thread (the owner thread), we must use the Post
method of the AsyncOperation
object (through the CallingThreadAsyncOp
member). The Post
method requires a special kind of delegate that only accepts a State
(as Object
) as parameter. You will see a little further down, in the WorkerBase
class, how we call the above SendOrPostCallBack
methods. Notice that these methods only perform a call to the overloaded signature while casting the state object as its strong-typed version.
We will be signalling three different states from our WorkerBase
to the manager: progression, exception, and regular ending.
''' (summary)
''' Sub called by the worker to signal a progression
''' (/summary)
''' (param name="progressData")
''' Progression data (if any)
''' (/param)
''' (remarks)(/remarks)
Friend Overloads Sub WorkerProgressInternalSignal(ByVal progressData As TProgressType)
'Prepare and raise the event for the owner to process
Dim e As Events.WorkerProgressEventArgs(Of TProgressType) = _
New Events.WorkerProgressEventArgs(Of TProgressType) _
(Identity, _
progressData)
RaiseEvent WorkerProgress(Me, e)
End Sub
''' (summary)
''' Sub called by the worker to signal an exception
''' (/summary)
''' (param name="workerException")
''' Exception
''' (/param)
''' (remarks)(/remarks)
Friend Overloads Sub WorkerExceptionInternalSignal(ByVal workerException As Exception)
If aIsAsynchonous AndAlso Not aWorkerThread Is Nothing Then
aWorkerThread.Join()
aWorkerThread = Nothing
End If
'Check if the results/exception have already been processed
'(because the owner was waiting for the worker to end)
If Not aCancelWorkerDoneEvent Then
'Prepare and raise the event for the owner to process
Dim e As Events.WorkerDoneEventArgs(Of TResultType) = _
New Events.WorkerDoneEventArgs(Of TResultType) _
(Identity, _
Nothing, _
workerException, _
aWorker.InterruptionRequested)
RaiseEvent WorkerDone(Me, e)
End If
'If the worker was running in asynchronous mode,
'we also need to post the "complete" message
If aIsAsynchonous Then
aCallingThreadAsyncOp.PostOperationCompleted(AddressOf DoNothing, Nothing)
End If
End Sub
''' (summary)
''' Sub called by the worker to signal the end of the work
''' (/summary)
''' (param name="result")
''' Worker result
''' (/param)
''' (remarks)(/remarks)
Friend Overloads Sub WorkerDoneInternalSignal(ByVal result As TResultType)
If aIsAsynchonous AndAlso Not aWorkerThread Is Nothing Then
aWorkerThread.Join()
aWorkerThread = Nothing
End If
'Check if the results/exception have already been processed
'(because the owner was waiting for the worker to end)
If Not aCancelWorkerDoneEvent Then
'Prepare and raise the event for the owner to process
Dim e As Events.WorkerDoneEventArgs(Of TResultType) = _
New Events.WorkerDoneEventArgs(Of TResultType) _
(Identity, _
result, _
Nothing, _
aWorker.InterruptionRequested)
RaiseEvent WorkerDone(Me, e)
End If
'If the worker was running in asynchronous mode,
'we also need to post the "complete" message
If aIsAsynchonous Then
aCallingThreadAsyncOp.PostOperationCompleted(AddressOf DoNothing, Nothing)
End If
End Sub
Now, these are the overloaded methods of the SendOrPostCallBack
versions. In asynchronous mode, the worker will be calling the SendOrPostCallBack
version (with state
as parameter) because it's required by the AsyncOperation.Post
action, and in synchronous mode, the worker will be directly calling the overload, which accepts the strongly-typed version of the parameter.
The three methods take care of creating the specific event argument instance required and raise the event, so the owner of the manager instance, or any other object that subscribed to the events, receive notification. For the WorkerExceptionInternalSignal
and WorkerDoneInternalSignal
methods, the event raised is the same, but the arguments will contain different things. In the first case, Exception
will not be null
, but Result
will. In the second case, Exception
will be null
, but Result
shouldn't be. There is, however, another little catch.
You will see below that the manager has a function that is called by its owner to "wait" for the worker to complete (WaitForWorker
). This function returns the same WorkerDoneEventArgs
class as the "exception" and "done" signals above. In my design, I concluded that if the owner at some point wants to wait for the worker to complete, it should also process the result immediately after, in the same method it requested the wait. Therefore, I decided that the event itself should not be raised when the owner is waiting for the worker to end. This is what the field aCancelWorkerDoneEvent
is used for.
Finally, when called asynchronously, these two "InternalSignal
" methods also call PostOperationCompleted
on the aCallingThreadAsyncOp
(AsyncOperation
) object. Notice that, right now, this in turn calls the private DoNothing
method, which... does nothing. I'm not too sure if the PostOperationCompleted
is absolutely required, but I decided not to take any chance and leave it there, but do nothing.
Let's go to the public
methods now.
''' (summary)
''' Called from the owner to start the worker
''' (/summary)
''' (param name="asynchronous")
''' Specifies if the worker must run in asynchronous mode (True) or not (False)
''' (/param)
''' (remarks)(/remarks)
Public Sub StartWorker(ByVal asynchronous As Boolean)
aIsAsynchonous = asynchronous
aCancelWorkerDoneEvent = False
If aIsAsynchonous Then
'Asynchronous mode - we need to create a thread
'and start the worker using this thread
aCallingThreadAsyncOp = AsyncOperationManager.CreateOperation(Nothing)
aWorkerThread = New Thread(New ThreadStart(AddressOf _
aWorker.StartWorkerAsynchronous))
aWorkerThread.Start()
Else
'Synchronous mode - simply call the worker's start method
aWorker.StartWorkerSynchronous()
End If
End Sub
This is where we start the worker. You give this method a simple Boolean
parameter to specify if you want to run in asynchronous (True
) or synchronous (False
) mode. If in asynchronous, the method takes care of creating the AsyncOperation
through the AsyncOperationManager.CreateOperation
service, creates the thread, and gives it the starting point StartWorkerAsynchronous
from the worker interface aWorker
, and starts the thread.
If in synchronous mode, it simply calls the StartWorkerSynchronous
method from the same worker interface aWorker
.
''' (summary)
''' Called from the owner to stop the worker
''' (/summary)
''' (remarks)(/remarks)
Public Sub StopWorker()
'Signal the worker to stop working
aWorker.StopWorker()
End Sub
This service simply calls its twin in the worker interface aWorker
, to request it to stop.
''' (summary)
''' Called from the owner to wait for the worker to complete
''' (/summary)
''' (remarks)(/remarks)
Public Function WaitForWorker() As Events.WorkerDoneEventArgs(Of TResultType)
If (Not aWorkerThread Is Nothing) AndAlso aWorkerThread.IsAlive Then
aWorkerThread.Join()
aWorkerThread = Nothing
End If
'Since the results (or exception) are returned through
'this function to be immediately processed by
'the owner waiting for the worker's completion, we cancel the WorkerDone event
aCancelWorkerDoneEvent = True
Return New Events.WorkerDoneEventArgs(Of TResultType) _
(Identity, _
DirectCast(aWorker.ResultObject, TResultType), _
aWorker.WorkerException, _
aWorker.InterruptionRequested)
End Function
As mentioned above, this service is used to wait for the worker to complete its job. When it does, it sets the flag aCancelWorkerDoneEvent
to prevent the WorkerDone
event from firing, and instead, directly returns the event arguments. Perhaps it's not the best design, since event arguments should probably only be used in events, but that's what I have to offer for now.
''' (summary)
''' Called from the owner to stop and wait for the worker
''' (/summary)
''' (remarks)(/remarks)
Public Function StopWorkerAndWait() As Events.WorkerDoneEventArgs(Of TResultType)
Dim result As Events.WorkerDoneEventArgs(Of TResultType) = Nothing
If (Not aWorkerThread Is Nothing) AndAlso aWorkerThread.IsAlive Then
StopWorker()
result = WaitForWorker()
End If
Return result
End Function
Finally, the third service is a mix of the two above. It first requests the worker to stop, which is only a request sent and performed on the child thread, so after the call is done, execution continues in the manager and it doesn't mean the worker has processed the interruption request.
Therefore, it also waits for the worker to process the interruption request and complete, to be absolutely sure that the thread is left inactive.
The WorkerBase Class
That's it for the manager class. Now, to the last piece of my wrapper design, the abstract WorkerBase
class.
''' (summary)
''' Abstract base worker class from which to inherit to create its own worker class
''' (/summary)
''' (typeparam name="TInputParamsType")
''' Type of the object that will contain the input parameters required by the worker
''' (/typeparam)
''' (typeparam name="TProgressType")
''' Type of the object that will contain the progress data
''' (/typeparam)
''' (typeparam name="TResultType")
''' Type of the object that will contain the results of the worker
''' (/typeparam)
''' (remarks)(/remarks)
Public MustInherit Class WorkerBase(Of TInputParamsType, _
TProgressType, TResultType)
Implements IWorker
First, you see the familiar generics we used in the manager and event argument classes above. This time, in addition to the TProgressType
and TResultType
, we also have a TInputParamsType
, which defines the type of object used to hold the parameters required by the worker class.
Since this class is defined as MustInherit
, it's in the declaration of the derived class that we will have to specify the generic types, like this:
Friend Class SubWorker1
Inherits SIGLR.Async.GenericWrapper.Worker.WorkerBase(Of SubWorker1Input, _
Integer, SubWorker1Result)
This specific worker class inherits from the WorkerBase
class, with input parameters as SubWorker1Input
, progress data as Integer
, and results as SubWorker1Result
.
Again, these generics are used throughout the base class, to allow strong-typed signatures, and to prevent the consumer from always having to cast every object.
''' (summary)
''' Constructor receiving the input parameters required by the worker
''' (/summary)
''' (param name="inputParams")Input parameters required by the worker(/param)
''' (remarks)(/remarks)
Protected Sub New(ByVal inputParams As TInputParamsType)
aInputParams = inputParams
End Sub
The base constructor only requires the input parameters, of the type specified by the generic TInputParamsType
. It will be passed to the DoWork
method, which is MustOverride
, and which contains the "real stuff" the worker will be running.
''' (summary)
''' Used between AsyncManager and WorkerBase, to become AsyncManagerTyped
''' (/summary)
''' (value)aAsyncManagerObject (Object)(/value)
''' (returns)The handle to the parent AsyncManager instance(/returns)
''' (remarks)(/remarks)
Friend Property AsyncManagerObject() As Object Implements IWorker.AsyncManagerObject
Get
Return aAsyncManagerObject
End Get
Set(ByVal value As Object)
aAsyncManagerObject = value
End Set
End Property
''' (summary)
''' Indicates if an interruption request was received
''' (/summary)
''' (value)aInterruptionRequested (Boolean)(/value)
''' (returns)True if an interruption request was received, otherwise False(/returns)
''' (remarks)(/remarks)
Protected Friend ReadOnly Property InterruptionRequested() As Boolean _
Implements IWorker.InterruptionRequested
Get
Return aInterruptionRequested
End Get
End Property
''' (summary)
''' Used to hold the results of the worker, as object
''' (/summary)
''' (value)aResult(/value)
''' (returns)Le résultat du travail(/returns)
''' (remarks)(/remarks)
Friend ReadOnly Property ResultObject() As Object _
Implements IWorker.ResultObject
Get
Return aResult
End Get
End Property
''' (summary)
''' Used to hold the exception (if any) that occurred in the worker
''' (/summary)
''' (value)aWorkerException(/value)
''' (returns)Exception (if any) that occurred in the worker(/returns)
''' (remarks)(/remarks)
Friend ReadOnly Property WorkerException() As Exception _
Implements IWorker.WorkerException
Get
Return aWorkerException
End Get
End Property
Since the base class implements the IWorker
interface, these are the required properties. Nothing really special about them, apart from AsyncManagerObject
and ResultObject
, which I already talked about in the interface section above.
''' (summary)
''' Function to hold the worker code (to actually do the work!)
''' (/summary)
''' (param name="inputParams")Paramètres d'entrée de type TTypeParametre(/param)
''' (returns)Result (as TTypeResultat) of the worker(/returns)
''' (remarks)(/remarks)
Protected MustOverride Function DoWork(ByVal inputParams _
As TInputParamsType) As TResultType
This is the DoWork
method, which is MustOverride
. It is given its parameters using the TInputTypeParamsType
generic, and returns its result as an object of the generic type TResultType
. Easy enough, don't you think?
This is where you need to be aware of how threads work. If there is any chance that objects used in this method may be shared by other threads (including the calling thread), these object classes should be made "thread-safe" with some locking mechanism. As mentioned in the Audience section of this article above, you may want to read Sacha Barber's article on threading to learn all of that.
''' (summary)
''' Called from the AsyncManager to start the worker in asynchronous mode
''' (/summary)
''' (remarks)(/remarks)
Friend Sub StartWorkerAsynchronous() Implements IWorker.StartWorkerAsynchronous
aIsAsynchronous = True
aInterruptionRequested = False
'We can strongly-type the AsyncManager parent object
aAsyncManagerTyped = DirectCast(aAsyncManagerObject, _
AsyncManager(Of TProgressType, TResultType))
'Set the SendOrPostCallback delegate to signal progress
aWorkerProgressInternalSignalCallback = New _
SendOrPostCallback(AddressOf aAsyncManagerTyped.WorkerProgressInternalSignal)
'Set the SendOrPostCallback delegate to signal the normal end of the worker
Dim workerDoneInternalSignalCallback As SendOrPostCallback = New _
SendOrPostCallback(AddressOf aAsyncManagerTyped.WorkerDoneInternalSignal)
'Set the SendOrPostCallback delegate to signal
'an exception that occurred in the worker
Dim workerExceptionInternalSignalCallback As SendOrPostCallback = New _
SendOrPostCallback(AddressOf _
aAsyncManagerTyped.WorkerExceptionInternalSignal)
'We must catch all exceptions
Try
'Do the actual work
aResult = DoWork(aInputParams)
'When the worker is done, we must signal the AsyncManager (on its own thread)
aAsyncManagerTyped.CallingThreadAsyncOp.Post(_
workerDoneInternalSignalCallback, aResult)
Catch ex As Exception
'When an exception occurs, we must signal the AsyncManager (on its own thread)
aWorkerException = ex
aAsyncManagerTyped.CallingThreadAsyncOp.Post(_
workerExceptionInternalSignalCallback, aWorkerException)
End Try
End Sub
OK... This is the starting method for the new thread created by the manager. Notice how we cast the AsyncManager
which was passed as object through the IWorker
interface's AsyncManagerObject
property. Now, it can be cast in its strong-type version AsyncManagerTyped
because we know the generic types to use (they're the same as the ones used in the class declaration).
The method also defines the three required SendOrPostCallBack
delegates, pointing to the AsyncManager
's three methods that receive a state object. Notice that the progress method is declared as class global attributes while the other two are simply variables inside the method. This is because the progress delegate is required outside of this method, as you'll see further down.
Finally, the method calls the DoWork
function. It is done inside a Try/Catch
so that if an unhandled exception occurs, it will be able to call the workerExceptionInternalSignalCallback
delegate with the exception as parameter. Otherwise, when the function returns, it will call the workerDoneInternalSignalCallback
delegate with the result as parameter. Both delegates are called through the Post
service of the manager's CallingThreadAsyncOp
, to be executed on the manager's thread.
''' (summary)
''' Called from the AsyncManager to start the worker in synchronous mode
''' (/summary)
''' (remarks)(/remarks)
Friend Sub StartWorkerSynchronous() Implements IWorker.StartWorkerSynchronous
aIsAsynchronous = False
aInterruptionRequested = False
'We can strongly-type the AsyncManager parent object
aAsyncManagerTyped = DirectCast(aAsyncManagerObject, _
AsyncManager(Of TProgressType, TResultType))
'We must catch all exceptions
Try
'Do the actual work
aResult = DoWork(aInputParams)
'When the worker is done, we must signal
'the AsyncManager (we're on the same thread)
aAsyncManagerTyped.WorkerDoneInternalSignal(aResult)
Catch ex As Exception
'When an exception occurs, we must signal
'the AsyncManager (we're on the same thread)
aWorkerException = ex
aAsyncManagerTyped.WorkerExceptionInternalSignal(aWorkerException)
End Try
End Sub
Although pretty similar to the asynchronous start worker method, this one is simpler and more straightforward, as it doesn't need any SendOrPostCallBack
delegate because everything runs on the same thread.
''' (summary)
''' Called by the derived class to signal a progression in the work process
''' (/summary)
''' (param name="progressData")
''' Object (as TProgressType) containing the progression data
''' (/param)
''' (remarks)(/remarks)
Protected Sub WorkerProgressSignal(ByVal progressData As TProgressType)
If aIsAsynchronous Then
'We are in asynchronous mode - the call
'must be performed on the calling thread
aAsyncManagerTyped.CallingThreadAsyncOp.Post(_
aWorkerProgressInternalSignalCallback, progressData)
Else
'We are in synchronous mode - the call can be performed directly
aAsyncManagerTyped.WorkerProgressInternalSignal(progressData)
End If
End Sub
This method's scope is Protected
, as it will be called by the derived class to signal a progression of some kind. It receives progression data as TProgressType
. If the worker is running in asynchronous mode, then the method uses the aWorkerProgressInternalSignalCallback
SendOrPostCallBack
delegate previously defined in the StartWorkerAsynchronous
method. It does so through the Post
service of the manager's CallingThreadAsyncOp
, to execute the delegate method on the manager's thread.
If not in asynchronous mode, then it calls the manager's WorkerProgressInternalSignal
method directly.
''' (summary)
''' Called from the AsyncManager to stop the worker
''' (/summary)
''' (remarks)(/remarks)
Friend Sub StopWorker() Implements IWorker.StopWorker
aInterruptionRequested = True
End Sub
This last method is called by its twin in the manager. It simply sets the aInterruptionRequested
flag to True
, so that the derived class can see the request and stop its work in a clean way.
How to Use the Wrapper / Manager
For each different functionality you want to run in a separate thread, you need to:
- Create a class which inherits from the
WorkerBase
class and put your work's code in theDoWork
method. - Optionally create a class to hold the input parameters required by the work you want done (which will become the
TInputParamsType
generic). - Optionally create a class to hold the progression data you want to marshal between the worker and the calling thread at various stages in your work (might be in a loop too) (which will become the
TProgressType
generic). - Optionally create a class to hold the results of the work, to be processed by the calling thread (which will become the
TResultType
generic).
If you don't need one or more of the generic types (let's say you don't need any input parameters, or your work doesn't report progression, or doesn't return a result), you can use any other standard type (like Integer
) as generics for the various declarations, it doesn't matter.
Then, from some point in your code where you want to start your parallel work, you use a new instance of the AsyncManager
class, declared WithEvents
to be able to handle the two possible events it will raise (WorkerDone
and WorkerProgress
), giving it a new instance of your worker class, and then call the manager's StartWorker
method.
Put some code in the event handlers you care for and that's it, you're done! Sounds easy? Take a look at the demo project included in the source code attached to this article. If you have any questions about it, I'll be happy to answer.
Still, in the near future (depending on my spare time availability!), I will write a second article to follow-up on this one, explaining in details how to use the library.
Conclusion
Of course, there is still a lot of room for improvement. For example, right now, a single manager instance does not handle more than one thread at a time. If you call StartWorker
repeatedly, you may get unexpected results.
You will probably see several other things that can be improved as well. To be honest, this is why I decided to post an article about it in the first time, so that I could benefit from the views and ideas of other experienced developers like you...
Version History
- 2014-05-22
- Created C# version of the demo
- 2009-04-22
- Updated the source code to include a C# version of the library, and also improve some of the code (fixing several Code Analysis warnings)
- Updated the code listings to the latest library version
- Added the Class Diagrams section
- Updated the How to Use the Wrapper/Manager section to announce a follow-up article to appear in the near future
- Added executable demo download
- Removed the Notice section
- 2009-04-18
- Added Audience section with link to Sacha Barber's introduction to threading article
- Added Version History section
- 2009-04-16
- Original version