Threading is commonly used by developers to increase the performance of applications. However, if used incorrectly threading can have the exact opposite effect. Bad threading logic can actually slow down an application, or worse, cause an application to have inconsistent exceptions. There really isn't much to be worried about though if you plan ahead and take the correct precautions.
About the ThreadPool
In C#, the ThreadPool is basically a collection of threads that you can use to run asynchronous tasks. For instance, if you had to call a database with ten different independent queries, you could post the calls to the ThreadPool and the calls would be executed asynchronously. The ThreadPool handles pretty much everything for you — the application developer just provides a method to execute and the ThreadPool executes it when a thread becomes available.
Although the ThreadPool is very easy to use, there are some tips and tricks that are helpful to know when dealing with it.
The flow of using the ThreadPool looks something like this:
- You post a method to be executed by the ThreadPool
- The ThreadPool waits for an idle thread
- When an idle thread is found the ThreadPool uses it to execute your method
- Your method executes and terminates
- The ThreadPool returns the thread and makes it available for other processing
There are some limits to the ThreadPool and you should be aware that it won't be the optimal approach to all of your threading needs. Please read the section below titled "ThreadPool Limits" for more information on this.
Using the ThreadPoolTo use the ThreadPool and instruct it to queue up a task you will call the ThreadPool.QueueUserWorkItem method. This method accepts a method name and will then execute the given method when a ThreadPool thread becomes available. A simple example of this is shown in Figure A. (The download version contains the actual code for all figures.)
As you can see this example simply queues up 10 items, passing the value of "id" to each item. The items then print out some data to the console. Note that since these are executing asynchronously they won't necessarily print out in order. The execution order is not guaranteed, even if you queue the items up in order.
Another note to make about this code is that we're passing a variable of type int into each thread instance. This works because ints are value types — if we were to use a reference type in this same situation then we could get overlap in our threads and get inconsistent results.
Communicating from the ThreadPoolThere are many times when you'll need to know when a thread has completed, or get some other information about the status of a thread. There are several ways to do this, but one of the easiest and most direct is to use events within the threads and subscribe to those events from your application. The code for this is shown Figure B.
In this example we're creating ten objects, subscribing to the OnWorkComplete event, and adding the objects to a List<T> object. After the worker classes have been created we then loop through the list and call the Start() method on them.The Start() method uses the ThreadPool internally, so the foreach statement doesn't actually wait for one object to complete its work before calling Start() on the next object in the list. The code for the WorkerClass class is shown in Figure C.
This class simply uses the ThreadPool.QueueUserWorkItem method to queue up the DoWork method, which has access to all of the class instance's members. The DoWork method then sleeps for one second to simulate work being done and then fires off the OnWorkCompleted event.
Using this type of setup encapsulates your threads and allows you to have a more granular control over them.
Multiple threads concurrently accessing the same variableAn issue you are bound to run into eventually is having more than one thread accessing a variable at the same time. A good example of this is shown in Figure D.
In this code we have ten threads all accessing the "data" variable which is of type Dictionary. At first glance this code looks ok. We're doing what we should be doing by checking to make sure the data variable doesn't contain a certain key before adding it. In a normal, synchronous, application this code would work without a hitch. However, when we introduce threading this code will break.
The reason is that even though we're making sure the key doesn't exist, other threads are accessing the same variable at roughly the same time. So it is possible for two threads to check that the key doesn't exist, both pass, and then after one adds the key the other cannot and an exception will be thrown. The Thread.Sleep call pretty much guarantees that we'll get an exception.To fix this type of issue we need to tell the runtime that the variable should only be accessed by one thread at a time. This is accomplished by using the lock() statement, which is shown Figure E.
The only major difference between this code and the code shown in Figure D is the lock() statement. The lock statement locks the variable passed into it and makes sure that only one thread is accessing the variable at a time. This ensures that once we check to make sure the key doesn't exist, no other threads will be able to add the key before we add it.
It is extremely important to use this type of functionality when threading is in use. The reason is that many times the errors won't show up during development (due to volume), and could make it all the way to a production environment before being caught.
The downfall of the ThreadPool is that the number of threads is finite and defaults to 25 threads per available processor. This means that if you queue up 30 tasks, the last five will have to wait for threads to become available from the pool before being executed. To get past the thread count restriction Microsoft has provided developers with a way to overwrite this number. You simply call the SetMaxThreads method and pass the number of threads you would like to have available.
A similar method is SetMinThreads. The ThreadPool doesn't really have all of the threads sitting idle and waiting for tasks — it only creates the number of threads that you request up to the value given to SetMaxThreads. So, if you expect to have the need for, say, 30 threads then you'll want to use SetMinThreads to make the minimum number of threads 30.
This will increase performance since the ThreadPool won't immediately create new threads when needed; it only does this on certain intervals. By default this interval is half of a second, so the creation of a thread (and delay in your application processing) can be up to half a second even if you haven't reached the maximum thread threshold.
In Part 2
In Part 2 of this series I will show you a couple of advanced methods to keep track of your threads and show how to handle thread exceptions. Stay tuned!