Introduction
Have you ever needed to run a whole lot of methods asynchronously? Did you need to limit the number running at once? How about having an interactive role in their control? Well, the NotifyingThreadQueue
might be just for you.
Comparison
Microsoft is really good at giving us the tools to do things we need in many different ways, but sometimes it can be hard to tell what to use. In my previous article, I had a lot of comments on this problem. I blame my poor presenting. This section is an effort to fix that.
System.Threading.Thread
Pros
- Around since 1.1(1.0?).
- A lot of people are very familiar with it.
Cons
- Suspend and resume are now deprecated in .NET 2.0.
- You need to rig up your own callbacks to get errors and completion.
Warning: Uncaught errors on threads will probably (always?) disappear, terminating the thread in the process.
- It does one operation asynchronously. When you need a whole lot of operations, you need a whole lot of threads.
System.Threading.ThreadPool
This is the closest to what I have made.
Pros
- All those of
System.Threading.Thread
.
- Allows many queued operations (in order, more or less). They will all finish eventually. When you put more than the maximum number of
ThreadPool
threads (25?) into the ThreadPool
, the others wait till one of the threads is free.
- Reuses old threads to avoid cost of reallocation(??). I remember hearing this, but I don't have it in print and I don't know how to verify it.
Cons
- You can't "pause/continue" the execution. Suspend and resume are now deprecated.
Note: When I refer to pause and continue, I do not refer directly to suspend and resume respectively. Instead I refer to the ability to pause the queue itself. That is to say that you let the current threads finish, but the remaining threads “pause” until you are want them to “continue.”
- You need to rig up your own callbacks to get errors and completion.
System.Windows.Forms.BackgroundWorker
Pros
- It’s a control in your VS2005 tool box. Drag and drop.
- It has events built in for complete and progress, but not error.
Cons
- .NET 2.0 only. This is not too big of a deal, unless you cannot use any 2.0 code at all.
- It does one operation asynchronously. When you need a whole lot of operations, you need a whole lot of
BackgroundWorkers
.
"Gang of Four" Delegate Pattern
I don't actually own this book so I can't vouch for its worth, but I hear it is good. This is here because the book I do have references their pattern as similar to this more well known pattern.
"Thilmany" Notifying Thread Manager
See references. I own this book. I like it.
Pros
- Supports a lot of operations at once.
- Pre-rigged in events for error and complete, but not progress. This seems odd since they want this to be presentation layer and don't have a progress event. Hmmm.
Cons
- Only one type of operation can be performed. No default operation with optional different operation selected at enqueue time.
- Uses delegates as callbacks not events. I think this is because they want this to be a presentation layer pattern and they want to avoid 2.0 cross-thread issues.
- You can't stop/pause/continue threads once they were handed to the manager.
- You can't control the max number of threads.
NotifyingThreadQueue
Pros
- Supports a lot of operations at once.
- Pre-rigged in events for error and complete, but not progress. You can add progress fairly easily, but be warned about cross-thread exceptions. (More on this later.)
- You can stop/pause/continue threads once they were handed to the manager.
- You can specify a default operation and at enqueue time you can hand it a different operation to use.
- You can control the max number of threads.
Cons
- .NET 2.0 only. I make use of generics since they allow strong typing. You can remove them and it should work in 1.1.
Queue Operations
The NotifyingThreadQueue
has several operations on it that will let you take a somewhat interactive approach to determining what the queue is doing.
Stop
This tells the NotifyingThreadQueue
to not accept any more work and to delete all the work it has waiting but didn't start. It lets the threads it already started on complete their operation. It does not call Abort()
. When you call this operation, the queue will call its event ThreadError
. This is a special case of the event. The event contains the unprocessed, as well as the exception “System.Threading.ThreadStateException("the Queue is stopping. No processing done")
.” I did this to let you reclaim the unprocessed objects.
Continue
This tells the NotifyingThreadQueue
to continue processing items in its queue. It will not continue once Stop()
has been called.
Pause
This tells the NotifyingThreadQueue
to continue to receive items to enqueue, but not to execute any more until Continue()
is called. You cannot Pause()
the Stop()
command.
Queue Events
QueueStateChanged
I made the queue have state so I could add the queue Operations. I added this since it didn't really take that much effort.
QueueState.Idle
: Ready to accept and run operations. There are no currently running operations.
QueueState.Running
: Ready to accept and run operations. There are currently running operations, so operations may be queued till some of the currently running operations finish.
QueueState.Pausing
: Ready to accept operations, but will not run any of them. There are currently operations running, but no more will be run until the Continue()
command is received.
QueueState.Paused
: Ready to accept operations, but will not run any of them. There are currently no operations running.
QueueState.Stopping
: Will not accept new operations. All enqueued operations are being thrown away. There are currently operations running. When all current operations are finished, the queue will be returned into the QueueState.Idle
state.
ThreadFinished
When an operation is finished it would be nice if you were somehow informed. This does that.
ThreadError
The ThreadError
event serves two purposes. The first is to let you know that there was an error in your operation. This is important for me to catch since it usually (always?) kills the thread and gives no error anywhere in VS. It also serves to tell you that the object was not processed because the queue was stopped. I think that’s a nice feature. This lets you reclaim the object if you really want to do something with it.
Things of Note
private void RunOpp(object o)
{
KeyValuePair<T, QueueOperationHandler<T>> kvp =
(KeyValuePair<T, QueueOperationHandler<T>>)o;
...
}
OMG. A cast in .NET 2.0! Hearsay, I say, Hearsay! Even in .NET 2.0, you don't have a method to start a thread that takes a strongly typed parameter. Even its new:
ParameterizedThreadStart(void(object) target).
One might think that this could have been made into a generic, but apparently it wasn't. This isn't too bad since the method is private
AND the value that gets put into it is always coming out of a strongly typed queue. So it isn't really that bad.
Gotchas
I made the NotifyingThreadQueue
to be used as a middle-tier component. However, I use it as a presentation layer component of the demo. This means that there are cross threading issues. If you are unfamiliar with this, it is a .NET 2.0 thing. Microsoft decided to not let a thread access a System.Windows.Forms.Control
created on a different thread. There are 2 solutions to this:
<yourcontrolname>.CheckForIllegalCrossThreadCalls = false;
CheckForIllegalCrossThreadCalls
is a static
bool on all System.Windows.Forms.Controls
. Basically, it says to ignore all of this type of errors and continue. This works, but if you get a LOT of these errors, then your application may actually slow down noticeably. If you have only a few of these, then this is a quick fix.
Note: Are you updating a progress bar? You will get one error per update.
Other instances of this error include setting the Text
field of any of your controls. I get mine by calling my <listviews>.Items.Add();
Use a delegate and invoke the call on the thread that created the control.
private delegate void AddCallback(ListView lv, string value);
private void AddLVIC(ListView lv, string value)
{
lv.Items.Add(value);
}
this.Invoke(new AddCallback(AddLVIC), new object[] { lvStatus, qs.ToString() });
This is a better way to do things. It does, however, get tedious to do this if you have a lot of updates or a lot of different type of updates. This is the real disadvantage of my design. But like I said, I am going to use this somewhere else.
Silent cross-thread exception dropping. I recognize that you might have the cross-thread exception issue, so I decided to catch and silently drop it. This is a double edged sword. QueueStateChangedInternal
, ThreadFinishedInternal
, and ThreadErrorInternal
are the culprits. If I find that I really don't like this, I might do another constructor. It just depends on my own use.
Demo Application
The demo application is nothing fancy. The 6 top buttons let you play with one or two operators and the max thread counts. The bottom 4 are for interactive viewing so you can see the start changed and that errors are caught and handled. You can also pause, stop, and continue the operation to see if you like the flow.
References
- .NET Patterns: Architecture, Design, and Process. Christian Thilmany. Addison Wesley. 2004
History
- 3rd August, 2006: Initial post