Introduction
Modern applications need to be aware of much more than their own context, and in particular, need to be aware of the power constraints of the machine that they run upon. More and more users run on laptops with battery constraints and we should program to allow them to conserve battery as it drains away.
The ThreadPool is a great scheme for asynchronous programming, but what about when you need to abort or suspend the Threads inside it? This class uses the WaitItem
interface of the ThreadPool
allowing you to pretty much replace this and provides extra functionality to allow Abort and Restart.
Background
I work on a laptop, and at the end of the day, I hibernate it. Once I get home, I attach to a different network on a different network adapter. Unfortunately, while working on an application with background asynchronous WebRequest
/WebResponse
s, I resumed the machine to Exceptions being thrown from deep inside the .NET framework in a thread that I had no chance of wrapping in a try
/catch
block (possibly, the same one that affects Visual Studio). I tried changing back to synchronous calls but that blocked the GUI and the exceptions still came. I needed a way to abort my WebRequest
before suspending the machine and run asynchronously.
I decided that the best way to do this was to run the call synchronously in another thread. A thread can be aborted before the machine suspends. A thread doesn't block the GUI. A thread can even come in a ThreadPool. Unfortunately, a System.Threading.ThreadPool
provides no access to the underlying thread(s), so there is no way to stop the thread once started.
Initially, I did not set out to replicate the ThreadPool
, I just wanted a simple way of starting and stopping a process running in a thread. That version took a delegate which was started in a thread and returned a result. However, that version would start 10-20 simultaneous threads together in my application, so I looked at queuing the work and using a single thread to run more than one delegate. After referring to the ThreadPool
interface and documentation, it became clear that I was redesigning the wheel; so, I took the relevant parts of the System.Threading.ThreadPool
interface, and re-implemented.
There are some methods of the ThreadPool
interface which relate to the number of Threads inside the pool, that I have chosen not to implement. According to the MSDN documentation, the standard ThreadPool
will keep a minimum number of threads running all the time. I have instead chosen to drop each and every thread if there is no work remaining, so that it is possible to end up with none running. I see no point in keeping them around if I only use the ThreadPool
for 5 seconds of processing every 30 minutes. See below for more details on this topic.
Update
This project has been substantially re-written to include a ThreadStartEvaluator
abstract class. Should you wish to change the logic for starting and stopping threads, then simply inherit from this base class and pass your new class into the constructor of the thread pool.
Using the code
Although your code can work perfectly well if you move from a System.Threading.ThreadPool
object to a CancellableThreadPool
, you should be aware that your threads are more likely to receive an abort signal. In order to provide the best possible code, we need to cover critical sections with try
/catch
blocks looking for ThreadAbortException
. These sections could simply perform cleanup of allocated objects, rather than just relying on garbage collection, or might abort other tasks such as existing WebRequest
/WebResponse
s, e.g.:
public class ExampleThread()
{
WebRequest wr = ...;
public void ThreadMethod()
{
try
{
wr.GetResponse(...);
}
catch (ThreadAbortException e)
{
wr.Dispose;
return;
}
}
}
Adding Cancelable thread pool to a new application
Start by adding the CancellableThreadPool
to your main application logic layer (this could be your main form), and constructing with the maximum number of queued items per thread.
CancellableThreadPool _threads = new CancellableThreadPool(...);
As you can see, the demo application uses a default max queue length of 2 (two). If you wish to have an application that will never queue a process and always allocates a new thread, specify 0 (zero). Alternatively, a large number (e.g., 65535) will give you a queue that always runs a single process at a time, but if you stop and restart, you are not guaranteed to process in the same order.
Next, define a method with a signature capable of being used as a WaitCallback
delegate, e.g.:
public void Update(object state)
...
To start a process, use the following, where demo
is the name of the object that holds the method,
ThreadDemo demo = new ThreadDemo(...);
_threads.QueueWorkItem(new WaitCallback(demo.Update) );
Supporting Threads in your application
Now that you have your code running multi-threadedly, things are going great until you try to update your System.Windows.Form Gui
. This example avoids the problem by refreshing from a timer and pulling the data out of the objects, all from the thread which created the GUI objects. This is not an ideal example.
Instead, I recommend adding methods to your Form
which provide simple functionality. This example could have included:
internal void ThreadStatus(ThreadDemo demo, string message)
The public
/internal
method then takes care of checking that we are on the correct Thread, and delegates the update to a private
method, i.e.:
internal void ThreadStatus(ThreadDemo demo, string message)
{
if (this.InvokeRequired == true)
this.Invoke(new ThreadStatusDelegate(realThreadStatus),
new object[2] {demo,message});
else
realThreadStatus(demo, message);
}
private delegate void ThreadStatusDelegate(ThreadDemo demo, string status);
private void realThreadStatus(ThreadDemo demo, string message)
{
GetListItem(demo).Text = message;
}
Code Features
This demonstration features a ThreadDemo
object that is trying to count up by 50. When restarted, it looks at its current count and adds 50, which defines where it will stop counting. Should an error occur in the abort and restart of the Thread, you will see threads that count in excess of 50. However, when a ThreadAbortException
is detected then the counter is reset to zero. This scenario is designed to demonstrate one method of resetting for use in more complicated processes.
CancellableThreadPool
The main code for reuse is in the CancellableThreadPool.cs file. The complexity of this class is in the ThreadRunner
method. This method works by extracting a queued work item and performing the delegate inside it. You are responsible for the error handling within your delegate. Should you allow an exception to propagate outside of your control, then the thread will terminate to avoid any later possible complications and help advance garbage collection. This whole method is Run by the threads started by this class.
ThreadStartEvaluator and derived classes
This code is used to determine when to start and stop threads. Override the EvaluateThreadStartStop
method to determine when new Threads should be started and stopped.
ThreadRunInfo
This class allows the state object to be passed to running Threads just as the System.Threading.ThreadPool
does.
Points of Interest
With the ThreadStartEvaluatorbyQueueSize
implementation, the number of running threads is dependant on the number of items in queue. You can set the maximum number of queued items per thread with the constructor as described above. This differs from the System.Threading.ThreadPool
interface, which,
- Can be changed after construction,
- Has a minimum number of threads running at all times.
Does anybody have any scenarios where these differences would cause any problems?
History
- v0.2a - 16th Dec 2004 - Rewrote to allow plug-in logic for controlling thread starting.
- V0.1a - 13th Sep 2004. Initial release.