One of the main disadvantages of the ThreadPool is that you send a method to it and you never know when the method has completed. You also will not know whether or not that method threw an exception. As you can imagine, not knowing that an exception has been thrown can have detrimental impacts on an application's performance.
This document presents a solution to those problems in the form of a class called ThreadManager. ThreadManager is a fairly simple class that basically acts as a proxy between you and the ThreadPool. Instead of using the ThreadPool directly, you use this class, and in return, you get extra functionality like task completion events and exception handling.
This blog post, including a sample Visual Studio Project complete with all of the sample code, is available in a TechRepublic download.
How ThreadManager worksI think the best way to demonstrate how ThreadManager works is to show you code snippets and then explain what is happening. First, let's look at how ThreadManager is used. (Figure A)
Create managed threads
This code simply creates a new ThreadManager object, subscribes to the OnItemComplete event, queues up 10 items to be executed, and then calls WaitForItemsToComplete(). WaitForItemsToComplete() is a blocking call and causes the calling thread to block until all threads are done executing in the ThreadManager.The interesting bit here is what the ThreadManager does internally after ThreadManager.QueueItem is called. The code for ThreadManager.QueueItem is shown in Figure B.
The first thing here is a call to Interlocked.Increment — this allows us to perform operations on objects that are being accessed by multiple threads at once. The next thing you'll notice is the instantiation of the MethodContainer object. This object acts as a wrapper around the WaitCallback and state objects sent into QueueItem.
The MethodContainer object works by holding an internal reference to the WaitCallback and state objects sent into its constructor. This allows the MethodContainer object to be instantiated and executed at different points in time.
ExecuteItemThis method takes the MessageContainer object passed into it and calls the ExecuteMethod() method. The ExecuteMethod instructs the MessageContainer object to execute the WaitCallback object that was originally sent to the ThreadManager.QueueItem method. The code for ExecuteMethod is shown in Figure D.
As you can see this method executes the WaitCallback object and catches any exceptions that might be thrown. If an exception is thrown, then the exception is assigned to the MethodContainer.Exception property. This is done because exceptions that are thrown from within ThreadPool threads do not bubble up to the parent thread. This can cause all sorts of problems with application performance, as well has headaches while trying to debug an issue.To determine whether or not an exception was thrown you will have to subscribe to the ThreadManager.OnItemComplete event as shown in Figure A. This event is fired as a result of the call to ItemComplete(MethodContainer) that is shown in Figure C. The code for the ItemComplete method is shown in Figure E.
ItemComplete methodThis method makes a call to Interlocked.Decrement and assigns the result to a local variable. That local variable is then passed along with the MethodContainer to the OnItemComplete event. This allows the calling thread to determine the number of threads remaining, and since the MethodContainer is also passed, the calling thread can determine exactly which child thread has completed and if an exception has occurred. An example of a calling thread handling the OnItemComplete event is shown in Figure F.
This code displays the thread that has completed, and if the MethodContainer.Exception object is not null it indicates that there was an exception. Similar to the OnItemComplete event, the ThreadManager class has an OnAllItemsComplete event. This event is thrown when all items have completed execution.
When to use ThreadManager
ThreadManager can be used anytime you need to spawn an additional thread and be notified when it is complete. A good example of this is a case when you need to make multiple calls to a database for several different customers.
For instance, say you needed to insert several items for an order a customer has placed. You don't want to continue processing until all of the items are inserted, but you would still like to take advantage of the ThreadPool so that each insert doesn't have to wait on the previous insert before it can be executed. In this case the ThreadManager would work great since it allows you to spawn multiple threads and then wait until they are all done before continuing with processing.
Of course, the ThreadManager class shouldn't be used for everything! It still uses the ThreadPool so it has similar limitations.
The TechRepublic Download
I would suggest that you take advantage of the TechRepublic Download that is associated with this article. The download includes a PDF version of the article as well as a Visual Studio Project file that contains all of the code for the ThreadManager.