Threading – Step by step
.NET is a powerful tool, that can be used by applications that demand high scalability. Without using the Threading namespace, I doubt we can unleash the power of the .NET platform. Depending on the application you write, in most cases ThreadPool should be enough to perform small task asynchronously (in a separated thread). ThreadPool provides no access to the underlying threads, so we have no control over the thread and there’s no way to stop a thread once it has been started. If there’s a need to gain contol over the thread (suspending, aborting) then we have to use Thread instead. But for complex application even the Thread class may need a bit of extra features.
My goal with this article is to:
- present the basic code from I have developed a complete class to support threading
- this article also demonstrates:
- how to use the System.Threading classes
- gives a basics about working with events and delegates
- and finally some basics about sybchronization will also be presented
So let’s go...
Threading basics
As I mentioned before the code described below was also used as basic for creating a class library, that allow an easy use of threading. If apply each part of this article plus if you ideas and a lot of work then the you can easily create a similar ThreadManager class as my own.
In .NET it’s really easy to use Threading. First create a class for the Thread with a public Worker function.
public class MyThread {
public void Worker() {
}
}
Let’s make a simple example: create a thread that will count down from an initial value to zero, then finish. The first thing 1 need to do is to pass the initial counter value to the class. I decided to use the constructor for this [using properties would be also fine, but then we would need also synchronization – and that will be presented later in this „lesson”].
public class MyThread {
private int _cnt;
public MyThread(int initialCounterValue) {
_cnt = initialCounterValue;
}
public void Worker() {
<FONT color=green> </FONT> while (_cnt > 0) {
Console.WriteLine("Counter = " + _cnt.ToString());
_cnt--;
Threading.Thread.Sleep(500);
}
<FONT color=green> // finish
// ...
</FONT> }
}
Now our thread class is finished. All we need to do is to make the thread work:
public static void Test() {
<FONT color=green> MyThread mt = new MyThread(100);
<FONT color=green> </FONT> Threading.ThreadStart ts = new Threading.ThreadStart(mt.Worker());
Threading.Thread t = new Threading.Thread(ts);
<FONT color=green> // start the thread</FONT>
t.Start();
<FONT color=green>
// join is used only in the test function
// the main thread (console appl.) will wait, until our thread finishes
</FONT> t.Join();
}
If you call the Start() function from your code, the thread will be started. In a console application you would normally need a call to the Console.ReadLine() function to prevent the main thread (the application) from exitting [if the main application exits, then all it’s threads will be also aborted (if they are backgound threads)]. Here we do not need this call to the Console.ReadLine(). Why? Because we of the line t.Join(). This line makes the main thread (from which the t.Join() was called) to wait until the thread t finishes.
Events and delegates
We’ve created a working thread in the previous chapter. Now we need to generate an event when the thread finishes. We have to create a delegate first. This allows us to pass a function as a parameter. Delegates are type-safe, so the function we will pass as a delegate has to have the same signature as the delegate declaration. Add this line after the declaration of the member variable _cnt.
public delegate void HandlerForFinish();
Delegate declaration can also have arguments, but we have no need for this at the moment. If you need an example how to use arguments with delegates, please examine the ThreadHandler v1.0 source code.
Now we have to declare the event. Put this line after the delegate declaration.
public event HandlerForFinish OnFinish;
And finally after the thread finishes, our event has to be invoked. This is done by adding the next line right after the // finish comment.
if (OnFinish != null) OnFinish();
As you see, it’s very simple. We first check, if an event is assigned and if sou, we simply invoke them (just like calling any other function in C#).
Now we have to modify the Test() function a little bit, to see the event working. We’ve created a delegate function, then we assigned that function to the OnFinish event.
So the „final” code is:
public class MyThread {
private int _cnt;
public delegate void HandlerForFinish();
public event HandlerForFinish OnFinish;
public MyThread(int initialCounterValue) {
_cnt = initialCounterValue;
}
public void Worker() {
<FONT color=green> while (_cnt > 0) {
System.Console.WriteLine("Counter = " + _cnt.ToString());
_cnt--;
System.Threading.Thread.Sleep(500);
}
<FONT color=green> if (OnFinish ! = null) OnFinish();
}
}
public void Test() {
<FONT color=green> MyThread mt = new MyThread(100);
<FONT color=green> </FONT> mt.OnFinish + = new HandlerForFinish(mt_OnFinish);
<FONT color=green> // create the threadstart and thread objects</FONT>
Threading.ThreadStart ts = new Threading.ThreadStart(mt.Worker());
Threading.Thread t = new Threading.Thread(ts);
<FONT color=green> // start the thread</FONT>
t.Start();
<FONT color=green> // join is used only in the test function
// the main thread (console appl.) will wait, until our thread finishes</FONT>
t.Join();
}
private void mt_OnFinish() {
<FONT color=green>// this is the time, we can remove the thread from the ThreadManager pool)</FONT>
Console.WriteLine("Thread has finished working...");
}
An alternative ThreadHandler class
In the previous chapter we got very close to the mentioned ThreadHandler class. Before moving to synchronization, we could re-arrange and modify our existing code a little bit and create a mini ThreadHandler.
The ThreadHandler will have a:
- Constructor
- Start method
- Abort method
- OnJob event
- OnFinish event
- OnAbort event
- OnTerminate
- OnException event
Also we will create an abstract class which will be used as a schema for the ThreadHandler. Also some public delegate methods will be needed...
The previous example will be transformed to a new structure. The benefits of the new structure:
- Always the same class will be used when threading is needed.
- From this point you can focus on creating the code that runs in the thread and you DO NOT HAVE to write extra code to running it in a separate thread.
- Locks will be added to the ThreadHandler’s published methods – so calling the ThreadHandler functions from different threads will be safe. Also locking mechanism is implemented inside the thread’s working function.
NOTE: The next chapter will give a short description on synchronization.
Delegate methods and EventArgs
For each event we have to create delegates. These will be used also in the public abstract class.
public delegate void HandlerForOnJob(ThreadHandlerEventArgs arg);
The OnJob event is executed after the thread starts. It’ll invoke the worker function and do the calculation in a separated thread.
public delegate void HandlerForOnAbort(ThreadHandlerEventArgs arg);
This event is invoked only, when aborting was demanded.
public delegate void HandlerForOnFinish(ThreadHandlerEventArgs arg);
The Finish event executes when the worker function executed successfully (and completely) ~ so the work was not aborted.
public delegate void HandlerForOnTerminate(ThreadHandlerEventArgs arg);
This event should be raised always when the thread is aborted or it has finished processing. It is for supporting clean-up processes (releasing some resources, removing the ThreadHandler from a collection...
public delegate void HandlerForOnException(ThreadHandlerExceptionArgs arg);
This event is raised, when there is an error inside any of the event-delegates (except OnTerminate).
As you se, we also need 2 special EventArg types:
public class ThreadHandlerEventArgs : EventArgs {
private ThreadHandler _threadHandler;
public ThreadHandler Handler {
get {
return_threadHandler;
}
}
public ThreadHandlerEventArgs(ThreadHandler threadHandler) {
_threadHandler = threadHandler;
}
}
<P>public class ThreadHandlerExceptionArgs : EventArgs {
private ThreadHandler _threadHandler;
private Exception _exception;
public ThreadHandler Handler {
get {
return_threadHandler;
}
}
public Exception Ex {
get {
return_exception;
}
}
public ThreadHandlerExceptionArgs(ThreadHandler th, Exception ex) {
_threadHandler = th;<
BR > _exception = ex;
}
}</P>
Abstract class
This class is not necessary. It only helps to make implementation easier.
public abstract class IThreadClass {
public IThreadClass() {
}
<FONT color=green> </FONT> public abstract void OnJob(ThreadHandlerEventArgs arg);
public virtual void OnAbort(ThreadHandlerEventArgs arg) {
}
public virtual void OnTerminate(ThreadHandlerEventArgs arg) {
}
public virtual void OnFinish(ThreadHandlerEventArgs arg) {
}
public virtual void OnException(ThreadHandlerExceptionArgs arg) {
throw arg.Ex;
}
}
ThreadHandler alternative
Comments
I will not include the full code from the project. Simply download the sources and check out the ThreadHandler.cs file. Also there is a simple test application (console app.) distributed along with the sources. There is a lot of comments in the ThreadHandler.cs file.
Please read at least the coments in the Worker() functions.
ThreadAbortException
This exception needs a little bit of extra care... When a call is made to Abort to terminate a thread, the system throws a ThreadAbortException. ThreadAbortException is a special exception that can be caught by application code, but is rethrown at the end of the catch block unless ResetAbort is called. ResetAbort cancels the request to abort, and prevents the ThreadAbortException from terminating the thread.
Synchronization basics
The .NET Framework has some classes for synchronisation. The most commonly used synchronisation „tool” is the mutual-exclusive lock (lock statement).
NOTE: locking is only allowed on objects.
Also it’s important avoid the use of the boxing conversion. (Boxing is the conversion of a value type to the type object. Boxing will create a new object from the value type. (For C++ coders: Boxing WILL NOT return the pointer to the original value. It will create a new object and return a reference to the newly created object).
The most usefull function of the lock statement is in it’s simplicity. It has also an special „advantage”: if your code inside a lock-statement throws an error, the exclusive lock is released immediately after leaving the block of the lock-statement.
How lock works: when an exclusive lock is acquired, all other exclusive lock requests (threads from where the lock is requested) are delayed (blocked), until the first exclusive lock is released...
NOTE: You can change the lock statement and use System.Threading.Monitor.Enter(...) and System.Threading.Monitor.Exit(...) for acquiring and releasing the exclusive lock.
About ThreadHandler v2.0
Beta version already exists... At the end of October testing should finish, also documentation should be complete 'till then.
DESCRIPTION
What is ThreadHandler? ThreadHandler is a class allowing you to use threading without additionally coding. To make you more happier, ThreadHandler comes with a ThreadManager class, which is actually an implementation of an advanced thread-pool for the ThreadHandler class.
SPECIAL FEATURES
Any function which is implemented by using ThreadHandler gains the following abilities:
- Stopping the working process in a given interval (you can define intervals inside the processing code where the processing may be stopped)
- Pausing the work process at a defined interval (just like stopping – you define when to allow and when to disable pausing)
- AfterPausing and BeforeResume an additional event is raised. This allows you to release resources after the thread has stopped processing and to re-allocate resources before continuing the work.
- Functions like WaitUntilPaused and WaitUntilTerminated will block the calling thread (probably the main thread) and invoke a function after the thread has stopped/paused.
OTHER FEATURES
- A threading class with all the important control functions : Start, Stop, AbortStopping, Pause, PauseImmediately, Abort
- Easy to use, event driven class : OnStart, OnJob, OnAbort, OnException, OnFinish, OnTerminate
- AfterSuspend and BeforeResume event!
- Support stopping and special pausing at any point (you can allow and deny special pausing and/or stopping for any part of your code, by simlpy calling a ThreadHandler function)
- You’ve got over 20 states, that are used to describe the state of the thread. [There are 20 state constants (enum), that are treated as a bit-field.]
- 2 function, that will block the caller thread, until ThreadHandler’s worker pauses or finishes (WaitUntilPaused, WaitUntilTerminated )
- Full access to the System.Threading.Thread object inside ThreadHandler.
- No need to create a separated (so called) thread-class with a worker function.
STATES, EVENTS, CONTROL FUNCTIONS AND PROPERTIES
Here is a list of the most control functions, properties of ThreadHandler.
States: Invalid, Unstarted, Exception, Running, PausedImmediately, PausingRequested, Pausing, Paused, WaitForResuming, Resuming, Resumed, StoppingRequested, Stopping, Stopped, Aborting, Aborted, Finishing, Finished, Terminating, Terminated.
Events: OnStart, OnJob, AfterSuspend, BeforeResume, OnAbort, OnException, OnFinish, OnTerminate
Control functions/properties: State, Start, Stop, AbortStopping, DisableAbortStopping, Pause, Resume, PauseImmediately, Abort, AllowPausing, AllowStopping, WaitUntilPaused, WaitUntilTerminated.
THE PREVIOUS VERSION (V1.0)
The „previous version” of this component is used in a working environment. It required a FSM (finite state machine) to achieve the same functionality.
In the 2nd version I got rid of this constraint. Also added some extra functions to make the class more better. There are functions that will make the caller thread to wait until the ThreadHandler achieves Paused, PausedImmediately or Terminated state. Also the number of states were increased and synchronization bugs were fixed.
Disclaimer
THIS CODE AND INFORMATION IS PROVIDED 'AS IS' WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.