Appreciating the System ThreadPool - ' Threads and ThreadPools ' (
Page 3 of 4 )
When you explicitly create a thread, your thread executes the delegate you
passed the constructor, and your thread dies when your delegate returns. By
default, your thread is a foreground thread.
By contrast, a TheadPool thread is a background thread that runs a
delegate to a method of an object that stores a delegate in an instance field.
The pooled thread's Thread delegate executes its stored delegate.
When that stored delegate returns, the pooled thread's Thread
delegate adds the pooled thread to a ready list within the system ThreadPool,
and then waits for its private wait handle to be set, which signals that the
pooled thread has a new stored delegate to execute.
This helps defer Thread destruction costs until the process
terminates. More importantly, the ThreadPool can drastically reduce
the number of threads a process needs to create by allowing threads to be reused.
You should explicitly create threads that will run for most of the life of the
application, and you should explicitly create threads that will do a lot of
computation. The ThreadPool is ideal for threads that come and go
without consuming many processor cycles. You should also probably explicitly
create threads when you need to change any of the thread's properties, such as Priority
or IsBackground; this will always be safer (and easier) than saving
and restoring a pool thread's state.
The system executes asynchronous delegates in ThreadPool threads.
Calling BeginInvoke on a delegate is both the easiest and most
efficient way to pass parameters to a thread delegate and/or to get results back.
If you are trying to take advantage of multiprocessing, using BeginInvoke
to run delegates asynchronously is an easy way to parallelize a foreach
loop.
Be sure to use a Semaphore to keep thread requests in line with
the system's set of multicore and/or hyperthreaded processors.
For example, given a declaration like delegate string Fetch(string
Resource), the simple thread at the start of this article could be
recoded, using the ThreadPool, as
Fetch Fn = delegate(string Filename)
{
using (StreamReader Reader = new StreamReader(Filename))
return Reader.ReadToEnd();
};
IAsyncResult Async = Fn.BeginInvoke(@"..\..\Program.cs", null, null);
// ...
string Source = Fn.EndInvoke(Async);
This is functionally identical to creating, starting, and joining the Thread
at the start of this article, but takes a bit less code (you don't have to Start
the Thread) and takes less time because you don't have to finalize
the thread, and you may not even have to create it.
In real code, you will probably already have a ReadFile or a GetURL
method that takes a resource name and returns a string. And you will
asynchronously call a Fetch delegate to a named method, instead of
using an anonymous method. Calling delegates asynchronously is the best
way to pass values to an agent and collect its results. But when your agent will
do something without returning any results, you might as well use an anonymous
method in lower-level code that calls the ThreadPool directly.
The ThreadPool.QueueUserWorkItem static method takes a WaitCallback
delegate and executes it. A WaitCallback delegate is a delegate to
a method that takes a single, object parameter and returns nothing: delegate
void WaitCallback (object state). The delegate executes right away, if a
thread is available or the pool size is within the configurable limits; if the ThreadPool
is empty, the delegate waits until a thread is returned to the pool. (The ThreadPool
has static methods to get and set the size of the pool.)
Because a WaitCallback delegate only gets a single, object
parameter, it's pretty ideally suited to an anonymous method that can carry all
its thread state information in captured variables. For example, you may have
accumulated a long Report string that you need to write to a given Filename.
Maybe running WriteFile(Filename, Report) will take milliseconds of
wall-clock time, but it will always succeed (or handle errors appropriately).
Using ThreadPool.QueueUserWorkItem, you can (re)use a thread to
capture the Filename and Report strings, and write the
file in a background thread:
ThreadPool.QueueUserWorkItem(delegate
{
WriteFile(Filename, Report);
});
Using anonymous methods as thread delegates that capture method parameters and/or
local variables maximizes information hiding and minimizes boiler plate. You don't
even have a private delegate type visible outside the method that spawns an
agent.