Skip to main content
Email Password   helpLost your password?

See the history section at the bottom for changes.

Introduction

Smart Thread Pool is a thread pool written in C#. The implementation was first based on Stephan Toub's thread pool with some extra features, but now it is far beyond the original. Here is a list of the thread pool features:

Why do you need a thread pool?

"Many applications create threads that spend a great deal of time in the sleeping state, waiting for an event to occur. Other threads might enter a sleeping state only to be awakened periodically to poll for a change or update status information. Thread pooling enables you to use threads more efficiently by providing your application with a pool of worker threads that are managed by the system. One thread monitors the status of several wait operations queued to the thread pool. When a wait operation completes, a worker thread from the thread pool executes the corresponding callback function".

MSDN, April 2004, ThreadPool Class [C#].

Smart Thread Pool features

When I wrote my application, I discovered that I needed a thread pool with the following features:

After I published the smart thread pool here, I found out that more features were required and some features had to change. So, the following is an updated list of the implemented features:

  1. The thread pool is instantiated.
  2. The number of threads dynamically changes.
  3. Work items return a value.
  4. The caller can wait for multiple or all the work items to complete.
  5. A work item can be cancelled if it hasn't been executed yet.
  6. The caller thread's context is used when the work item is executed (limited).
  7. Usage of minimum number of Win32 event handles, so the handle count of the application won't explode.

Because of features 3 and 5, the thread pool no longer complies to the .NET ThreadPool, and so I could add more features.

See the additional features section below for the new features added in this version.

Additional features added in December 2004

  1. Every work item can have a PostExecute callback. This is a method that will be called right after the work item execution has been completed.
  2. The user can choose to automatically dispose off the state object that accompanies the work item.
  3. The user can wait for the Smart Thread Pool to become idle. This is useful for waiting till the completion of all the work items in the queue.
  4. The exception handling is changed, so if a work item throws an exception, it is re-thrown at GetResult(), rather than firing an UnhandledException event. Note that PostExecute exceptions are always ignored.

New features added in January 2006

  1. Work items have priority.
  2. The caller thread's HTTP context can be used when the work item is executed (improves 6.).
  3. Work items group.
  4. The caller can create thread pools and work item groups in suspended state.
  5. Threads have priority.

What about the .NET ThreadPool?

The Windows system provides one .NET ThreadPool for each process. The .NET ThreadPool can contain up to 25 (by default) threads per processor. It is also stated that the operations in .NET ThreadPool should be quick to avoid suspension of the work of others who use the .NET ThreadPool. Note that several AppDomains in the same process share the same .NET ThreadPool. If you want a thread to work for a long period of time, then the .NET ThreadPool is not a good choice for you (unless you know what you are doing). Note that each asynchronous method call from the .NET Framework that begins with "Begin�" (e.g., BeginInvoke, BeginSend, BeginReceive, etc.) uses the .NET ThreadPool to run its callback.

This thread pool doesn't comply with the requirements 1, 5, 6, 8, 9, 10, 12, 13, 14, 15, 16.

Note that the requirements 3 and 4 are implemented in .NET ThreadPool with delegates.

What about Stephen Toub's thread pool?

Toub's thread pool is a better choice than the .NET ThreadPool, since a thread from his pool can be used for a longer period of time, without affecting the asynchronous method calls. Toub's thread pool uses static methods; hence you cannot instantiate more than one thread pool. However, this limitation applies per AppDomain rather than the whole process. The main disadvantage of Toub's thread pool over the .NET TheradPool is that Toub creates all the threads in the pool at the initialization point, while the .NET ThreadPool creates threads on the fly.

This thread pool doesn't comply with the requirements 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16.

The Smart Thread Pool design and features

As I mentioned before, the Smart Thread Pool is based on Toub's thread pool implementation. However, since I have expanded its features, the code is no longer similar to the original one.

Features implementation:

  1. The thread pool is instantiated.

    The reason I need an instantiable thread pool is because I have different needs. I have work items that take a long time to execute and I have work items that take very short time to execute. Executing the same type of work items on the same thread pool may cause some serious performance or response problems.

    To implement this feature, I just copied Toub's implementation and removed the static keyword from the methods. That's the easy part of it.

  2. The number of threads dynamically changes.

    The number of threads dynamically changes according to the workload on the threads in the pool, with lower and upper constraints for the number of threads in the pool. This feature is needed so we won't have redundant threads in the application.

    This feature is a real issue and is the core of the Smart Thread Pool. How do you know when to add a new thread and when to remove it?

    I decided to add a new thread every time a new work item is queued and all the threads in the pool are busy. The formula for adding a new thread can be summarized to:

    (InUseThreads + WaitingCallbacks) > WorkerThreads

    where WorkerThreads is the current number of threads in the pool, InUseThreads is the number of threads in the pool that are currently working on a work item, and WaitingCallbacks is the number of waiting work items. (Thanks to jrshute for the comment.)

    The SmartThreadPool.Enqueue() method looks like this:

    private void Enqueue(WorkItem workItem) 
    { 
        // Make sure the workItem is not null 
    
        Debug.Assert(null != workItem); 
    
        // Enqueue the work item
    
        _workItemsQueue.EnqueueWorkItem(workItem); 
    
        // If all the threads are busy then try 
    
        // to create a new one 
    
        if ((InUseThreads + WaitingCallbacks) > _workerThreads.Count) 
        { 
            StartThreads(1); 
        } 
    }

    When the number of threads reaches the upper limit, no more threads are created.

    I decided to remove a thread from the pool when it is idle (i.e. the thread doesn't work on any work item) for a specific period of time. Each time a thread waits for a work item on the work item's queue, it also waits for a timeout. If the waiting exceeds the timeout, the thread should leave the pool, meaning the thread should quit if it is idle. It sounds like a simple solution, but what about the following scenario: assume that the lower limit of threads is 0 and the upper limit is 100. The idle timeout is 60 seconds. Currently, the thread pool contains 60 threads, and each second a new work item arrives, and it takes a thread one second to handle a work item. This means that, each minute, 60 work items arrive and are handled by 60 threads in the pool. As a result, no thread exits, since no thread is idle for a full 60 seconds, although 1 or 2 threads are enough to do all the work.

    In order to solve the problem of this scenario, you have to calculate how much time each thread worked, and once in a while exit the threads that don't work for enough time in the timeout interval. This means, the thread pool has to use a timer (uses the .NET ThreadPool) or a manager thread to handle the thread pool. To me, it seems an overhead to use a special thread to handle a thread pool.

    This led me to the idea that the thread pool mechanism should starve the threads in order to let them quit. So, how do you starve the threads?

    All the threads in the pool are waiting on the same work items queue. The work items queue manages two queues, one for the work items and one for the waiters (the threads in the pool). Since the trivial work items queue works, the first arrived waiter for a work item gets it first (queue), and so you cannot starve the threads.

    Have a look at the following scenario:

    The thread pool contains four threads. Let's name them A, B, C, and D. Every second a new work item arrives, and it takes less than one second and a half to handle each work item:

    Work item arrival time (sec)

    Work item work duration (sec)

    Threads queue state

    The thread that will execute the arrived work item

    00:00:00

    1.5

    A, B, C, D

    A

    00:00:01

    1.5

    B, C, D

    B

    00:00:02

    1.5

    C, D, A

    C

    00:00:03

    1.5

    D, A, B

    D

    In this scenario, all the four threads are used, although two threads could handle all the work items.

    The solution is to implement the waiters queue as a stack. In this implementation, the last arrived waiter for a work item gets it first (stack). This way, a thread that just finished its work on a work item waits as the first waiter in the queue of waiters.

    The previous scenario will look like this with the new implementation:

    Work item arrival time (sec)

    Work item work duration (sec)

    Threads queue state

    The thread that will execute the arrived work item

    00:00:00

    1.5

    A, B, C, D

    A

    00:00:01

    1.5

    B, C, D

    B

    00:00:02

    1.5

    A, C, D

    A

    00:00:03

    1.5

    B, C, D

    B

    Threads A and B handle all the work items, since they get back to the front of the waiters queue after they have finished. Threads C and D are starved, and if the same work items are going to arrive for a long time, then the threads C and D will have to quit.

    The thread pool doesn't implement a load balancing mechanism, since all the threads run on the same machine and take the same CPUs. Note that if you have many threads in the pool, then you will prefer minimum number of threads to do the job, since each context switch of the threads may result in paging of the threads' stacks. Less working threads means less paging of the threads' stacks.

    The work items queue implementation causes threads to starve, and the starved threads quit. This solves the scenario I mentioned earlier without using any extra thread.

    The second feature also states that there should be a lower limit to the number of threads in the pool. To implement this feature, every thread that gets a timeout, because it doesn't get any work items, checks whatever it can to quit. The Smart Thread Pool allows the thread to quit only if the current number of threads is above the lower limit. If the number of threads in the pool is below or equal to the lower limit, then the thread stays alive.

  3. Work items return a value.

    This feature is very useful in cases you want to know the result of a work item.

    The .NET ThreadPool supports this feature via delegates. Each time you create a delegate you get for free BeginInvoke() and EndInvoke() methods. The BeginInvoke() queues the method and its parameters on the .NET ThreadPool and the EndInvoke() returns the result of the method. The delegate class is sealed so I couldn't override the BeginInvoke() and EndInvoke() methods. I took a different approach to implement this.

    First, the work item callback delegate returns an object rather than void:

    public delegate object WorkItemCallback(object state);

    Second, the SmartThreadPool.QueueWorkItem() method returns a reference to an object that implements the IWorkItemResult interface. The caller can use this object to get the result of the work item. The interface is similar to the IAsyncResult interface:

    public interface IWorkItemResult 
    { 
        /// Get the result of the work item. 
    
        /// If the work item didn't run yet then the caller waits
    
        /// until timeout or until the cancelWaitHandle is signaled. 
    
        /// If the work item threw then GetResult() will rethrow it. 
    
        /// Returns the result of the work item. 
    
        /// On timeout throws WorkItemTimeoutException. 
    
        /// On cancel throws WorkItemCancelException.
    
        object GetResult(
            int millisecondsTimeout,
            bool exitContext,
            WaitHandle cancelWaitHandle); 
        
        /// Some of the GetResult() overloads
    
        /// get Exception as an output parameter. 
    
        /// In case the work item threw
    
        /// an exception this parameter is filled with 
    
        /// it and the GetResult() returns null. 
    
        /// These overloads are provided
    
        /// for performance reasons. It is faster to 
    
        /// return the exceptions as an output
    
        /// parameter than rethrowing it.
    
        object GetResult(..., out Exception e);
            
        /// Other GetResult() overloads.
    
        ...
        
        /// Gets an indication whether
    
        /// the asynchronous operation has completed.
    
        bool IsCompleted { get; }
        
        /// Returns the user-defined object
    
        /// that was provided in the QueueWorkItem.
    
        object State { get; }
        
        /// Cancel the work item if it wasn't executed yet.
    
        /// Returns true on success or false if the work item
    
        /// is in progress or already completed.
    
        bool Cancel();
        
        /// Added in the new version
    
       
        /// Get the work item's priority
    
        WorkItemPriority WorkItemPriority { get; }
    
        /// Returns the result, same as GetResult(). 
    
        /// Note that this property blocks the caller 
    
        /// like GetResult(). 
    
        object Result { get; }
    
        /// Returns the exception, if occured, otherwise returns null.
    
        /// This function is not blocking like the Result property.
    
        object Exception { get; }
    }

    The code examples in the section below shows some snippets of how to use it.

    To get the result of the work item, use the Result property or the GetResult() method. This method has several overloads. In the interface above, I have written only some of them. The other overloads use less parameters by giving default values. The GetResult() returns the result of the work item callback. If the work item hasn't completed then the caller waits until one of the following occurs:

    GetResult() return reason GetResult() return value
    The work item has been executed and completed. The result of the work item.
    The work item has been canceled. Throws WorkItemCancelException.
    The timeout expired. Throws WorkItemTimeoutException.
    The cancelWaitHandle is signaled. Throws WorkItemTimeoutException.
    The work item threw an exception. Throws WorkItemResultException with the work item's exception as the inner exception.

    There are two ways to wait for a single work item to complete:

    1. The following function uses the GetResult() method which blocks the caller until the result is available:
      private void WaitForResult1(IWorkItemResult wir)
      {
          wir.GetResult();
      }
    2. The following function is not recommended, because it uses a busy wait loop. You can use it if you know what you are doing:
      private void WaitForResult2(IWorkItemResult wir)
      {
          while(!wir.IsCompleted)
          {
              Thread.Sleep(100);
          }
      }
  4. The caller can wait for multiple or all work items to complete.

    This feature is very useful if you want to run several work items at once and then wait for all of them to complete. To do so, the Smart Thread Pool supports the waiting for several work item results at once. The SmartThreadPool class has two static methods for this: WaitAny() and WaitAll() (they have several overloads). Their signature is similar to the WaitHandle equivalent methods except that in the SmartThreadPool case, it gets an array of IWorkItemResult objects instead of WaitHandle objects.

    The following snippets show how to wait for several work item results at once. Assume wir1 and wir2 are of type IWorkItemResult. You can wait for both work items to complete:

    // Wait for both work items complete 
    
    SmartThreadPool.WaitAll(new WaitHandle [] { wir1, wir2});

    Or, for any of the work items to complete:

    // Wait for at least one of the work items complete 
    
    SmartThreadPool.WaitAny(new WaitHandle [] { wir1, wir2});

    The WaitAll() and WaitAny() methods are overloaded, so you can specify timeout, exit context, and cancelWaitHandle (just like in the GetResult() method mentioned earlier).

    Note that in order to use WaitAny() and WaitAll(), you need to work in MTA, because internally I use WaitHandle.WaitAny() and WaitHandle.WaitAll() which requires it. If you don't do that, the methods will throw an exception to remind you.

    Also note that Windows supports WaitAny() of up to 64 handles. The WaitAll() is more flexible and I re-implemented it so it is not limited to 64 handles.

    See in the examples section below the code snippets for WaitAll and WaitAny.

  5. A work item can be cancelled if it hasn't been executed yet.

    This feature states that the Smart Thread Pool supports the canceling of a work item. The cancel is limited to work items that haven't been executed yet, that is, the work items that are still in the work items queue. When a work item is canceled, it is just marked as cancelled and is not removed from the work items queue.

    During the processing of a work item, it has one of the four states specified in the following enum:

    private enum WorkItemState
    {
        /// The work item is in the queue.
    
        InQueue,
    
        /// A thread from the pool is executing the work item.
    
        InProgress,
    
        /// The work item execution has been completed.
    
        Completed,
    
        /// The work item has been cancelled.
    
        Canceled,
    }

    The work item state is a private member of the work item and cannot be seen from the outside.

    In order to cancel a work item, use the Cancel() method provided in the IWorkItemResult interface that was returned by the SmartThreadPool.EnqueueWorkItem() method. The Cancel() may return false if the state of the work item is not 'InQueue'.

    If a work item has been successfully canceled then its IWorkItemResult is also affected.

  6. The caller thread's context is used when the work item is executed (limited).

    This feature should be elementary, but it is not so simple to implement. In order to pass the thread's context, the caller thread's CompressedStack should be passed. This is impossible since Microsoft blocks this option with security. Other parts of the thread's context can be passed. These include:

    The first three belong to the System.Threading.Thread class (static or instance) and are get/set properties. However, the last one is a read only property. In order to set it, I used reflection, which slows down the application. If you need this context, just remove the comments from the code.

    To simplify the operation of capturing the context and then applying it later, I wrote a special class that is used internally and does all that stuff. The class is called CallerThreadContext and it is used internally. When Microsoft unblocks the protection on the CompressedStack, I will add it there.

    The caller thread's context is stored when the work item is created, within the EnqueueWorkItem() method. Each time a thread from the pool executes a work item, the thread's context changes in the following order:

    1. The current thread context is captured.
    2. The caller thread context is applied.
    3. The work item is executed.
    4. The current old thread context is restored.
  7. Usage of minimum number of Win32 event handles, so the handle count of the application won't explode.

    The seventh feature is a result of Kevin's comment on the earlier version of Smart Thread Pool. It seemed that the test application consumed a lot of handles (Handle Count in the Task Manager) without freeing them. After a few tests, I got to the conclusion that the Close() method of ManualResetEvent class doesn't always release the Win32 event handle immediately, and waits for the garbage collector to do that. Hence, running the GC explicitly releases the handles.

    To make this problem less acute, I used a new approach. First, I wanted to create less number of handles, second, I wanted to reuse the handles I had already created. Therefore, I need not expose any WaitHandle but use them internally and then close them.

    In order to create fewer handles, I created the ManualResetEvent objects only when the user asks for them (lazy creation). For example, if you don't use the GetResult() of the IWorkItemResult interface then a handle is not created. Using SmartThreadPool.WaitAll() and SmartThreadPool.WaitAny() creates a handle.

    The work item queue created a lot of handles since each new wait for a work item created a new ManualResetEvent. Hence, a handle for each work item. The waiters of the queue are always the same threads and a thread cannot wait more than once. So now, every thread in the thread pool has its own ManualResetEvent and reuses it. To avoid coupling of the work items queue and the thread pool implementation, the work items queue stores a context inside the TLS (Thread Local Storage) of the thread.

  8. Work item can have a PostExecute callback. This is a method that will be called right after the work item execution has been completed.

    A PostExecute is a callback method that is called right after the work item execution has been completed. It runs in the same context of the thread that executed the work item. The user can choose the cases in which the PostExecute is called. The options are represented in the CallToPostExecute flagged enumerator:

    [Flags]
    public enum CallToPostExecute
    {
        Never                    = 0x00,
        WhenWorkItemCanceled     = 0x01,
        WhenWorkItemNotCanceled  = 0x02,
        Always = WhenWorkItemCanceled | WhenWorkItemNotCanceled,
    }

    Explanation:

    The SmartThreadPool has a default CallToPostExecute value of CallToPostExecute.Always. This can be changed during the construction of the SmartThreadPool in the STPStartInfo class argument. Another way to give the CallToPostExecute value is in one of the SmartThreadPool.QueueWorkItem overloads. Note that as opposed to the WorkItem execution, if an exception has been thrown during the PostExecute, then it is ignored. The PostExecute is a delegate with the following signature:

    public delegate void PostExecuteWorkItemCallback(IWorkItemResult wir);

    As you can see, the PostExecute receives as an argument of type IWorkItemResult. It can be used to get the result of the work item, or any other information made available by the IWorkItemResult interface.

  9. The user can choose to automatically dispose off the state object that accompanies the work item.

    When the user calls the QueueWorkItem, he/she can provide a state object. The state object usually stores specific information, such as arguments, that should be used within the WorkItemCallback delegate.

    The state object life time depends on its contents and the user's application. Sometimes, it is useful to dispose off the state object just after the work item has been completed. Especially if it contains unmanaged resources.

    For this reason, I added a boolean to the SmartThreadPool that indicates to call Dispose on the state object when the work item has been completed. The boolean is initialized when the thread pool is constructed with the STPStartInfo. The Dispose is called only if the state object implements the IDisposable interface. The Dispose is called after the WorkItem has been completed and its PostExecute has run (if a PostExecute exists). The state object is disposed even if the work item has been canceled or the thread pool has been shutdown.

  10. The user can wait for the Smart Thread Pool to become idle. This is useful for waiting till the completion of all the work items in the queue.

    This feature enables the user to wait for the Smart Thread Pool to become idle. The Smart Thread Pool becomes idle when the work items queue is empty and all the threads have completed their last work item.

    This is useful in case you want to run a batch of work items and then wait for all of them to complete. It saves you from handling the IWorkItemResult objects in case you just want to wait for all of the work items to complete.

    To take advantage of this feature, use the SmartThreadPool.WaitForIdle() method. It has several overloads which provide a timeout argument. The WaitForIdle() method is not static and should be used on a SmartThreadPool instance.

    The SmartThreadPool always keeps track of how many work items it has. When a new work item is queued, the number is incremented. When a thread completes a work item, the number is decremented. The total number of work items includes the work items in the queue and the work items that the threads are currently working on.

    The WaitForIdle() mechanism works with a private ManualResetEvent. When a work item is queued, the ManualResetEvent is reset (changed to non signaled state). When the work items count becomes zero (initial state of the Smart Thread Pool), the ManualResetEvent is set (changed to signaled state). The WaitForIdle() method just waits for the ManualResetEvent to implement its functionality.

    See the example below.

  11. The exception handling is changed, so if a work item throws an exception, it is rethrown at GetResult(), rather than firing an UnhandledException event. Note that PostExecute exceptions are always ignored.

    After I did some reading about delegates and their implementation, I decided to change the way the SmartThreadPool treats exceptions. In the previous versions, I used an event driven mechanism. Entities were registered to the SmartThreadPool.UnhandledException event, and when a work item threw an exception, this event was fired. This is the behavior of Toub�s thread pool.

    .NET delegates behave differently. Instead of using an event driven mechanism, it re-throws the exception of the delegated method at the EndInvoke(). Similarly, the SmartThreadPool exception mechanism is changed so that exceptions are no longer fired by the UnhandledException event, but rather re-thrown again when IWorkItemResult.GetResult() is called.

    Note that exceptions slow down .NET and degrade the performance. .NET works faster when no exceptions are thrown at all. For this reason, I added an output parameter to some of the GetResult() overloads, so the exception can be retrieved rather than re-thrown. The work item throws the exception anyway, so re-throwing it will be a waste of time. As a rule of thumb, it is better to use the output parameter than catch the re-thrown exception.

    The GetResult() can be called unlimited number of times and it re-throws the same exception each time.

    Note that PostExecute is called, as and when needed, even if the work item has thrown an exception. Of course, PostExecute implementation should handle exceptions if it calls GetResult().

    Also note that if the PostExecute throws an exception then its exception is ignored.

    See the example below.

  12. Work items have priority.

    Work items priority enables the user to order work items at run time. Work items are ordered by their priority. High priority is treated first. There are five priorities:

    public enum WorkItemPriority 
    { 
        Lowest, 
        BelowNormal, 
        Normal, 
        AboveNormal, 
        Highest,
    }

    The default priority is Normal.

    The implementation of priorities is quite simple. Instead of using one queue that keeps the work items sorted inside, I used one queue for each priority. Each queue is a FIFO. When the user enqueues a work item, the work item is added to the queue with a matching priority. When a thread dequeues a work item, it looks for the highest priority queue that is not empty.

    This is the easiest solution to sort the work items.

  13. The caller thread's HTTP context can be used when the work item is executed.

    This feature improves 6, and was implemented by Steven T. I just replaced my code with that implementation.

    With this feature the Smart Thread Pool can be used with ASP.NET to pass the context of HTTP between the caller thread and the thread in the pool that will execute the work item.

  14. Work items group.

    This feature is the highlight of this version. It enables the user to execute a group of work items specifying the maximum level of concurrency.

    For example, assume that we have a server that fulfills our requests. Each request is a combination of a set of work items that should run in a sequence. More than one request may be executed at the same time. The Work Items Group intends to solve such cases in a simple way. Here is a code snippet that uses a Work Items Group:

    ...
    
    // Create a SmartThreadPool
    
    SmartThreadPool smartThreadPool = new SmartThreadPool();
    
    // Create a work items group that processes 
    
    // one work item at a time
    
    IWorkItemsGroup workItemsGroup = 
         smartThreadPool.CreateWorkItemsGroup(1);
    
    // Queue some work items, the state 
    
    // object passes a context between the 
    
    // work items.
    
    workItemsGroup.QueueWorkItem(
        new WorkItemCallback(this.DoSomeWork1), state);
    workItemsGroup.QueueWorkItem(
        new WorkItemCallback(this.DoSomeWork2), state);
    
    // Wait for the completion of all work 
    
    // items in the work items group
    
    workItemsGroup.WaitForIdle();
    
    ...

    As you can see from the snippet a Work Items Group is attached to an instance of a Smart Thread Pool. The Work Items Group doesn't have threads of its own, but rather uses the threads of the Smart Thread Pool. It also has an interface similar to the Smart Thread Pool, so it can be used in the same way and replaced when needed.

    The WorkItemsGroup has a priority queue (the same as the SmartThreadPool). The queue stores the work items of the WorkItemsGroup. The WorkItemsGroup dequeues the work item with the highest priority at the head of the queue and queues it into the SmartThreadPool with the same priority.

    The WorkItemsGroup is responsible for managing the maximum level of concurrency of its work items. Once a work item is queued into the WorkItemsGroup, it checks how many work items it has in the SmartThreadPool. If this number is less than the maximum level of concurrency, it queues the work item into the SmartThreadPool. If this number is equal (it cannot be greater) then the WorkItemsGroup stores the work item in its own priority queue.

    In case the WorkItemsGroup is created in suspend mode, it will store the work items in its queue until it is started. When it is started it will queue the work items into the SmartThreadPool up to the maximum level of concurrency.

    Note that the WorkItemsGroup only has a maximum level of concurrency and not a minimum or exact value. It is possible to have a concurrency level of 3, and have non work items executing, since they are waiting in the SmartThreadPool queue.

    To accomplish the concurrency level, the WorkItemsGroup registers to the completion event of its work items. The event is used internally and is not exposed to the user. Once registered, the WorkItemsGroup will get an event when its work item is completed. The event will trigger the WorkItemsGroup to queue more work items into the SmartThreadPool. The event is the only way to accomplish the concurrency level. When I tried to do it with PostExecute I got fluctuating WaitForIdle.

    Another advantage of the WorkItemsGroup is that it can cancel all its work items that haven't been executed yet in one method with a complexity of O(1). The WorkItemsGroup does so by attaching an object to each one of its work items that indicates if the WorkItemsGroup has been cancelled. When a work item is about to be executed, it is asked for its current state (InQueue, InProgress, Completed, or Canceled). The final state considers this object's value to know if the work item was cancelled.

    See the examples below.

  15. The caller can create thread pools and work items groups in suspended state.

    When a Smart Thread Pool is created, by default, it starts its threads immediately. However, sometimes you need to queue a few work items and only then start executing them.

    In these cases, you can create the Smart Thread Pool and the Work Items Group in a suspended state. When you need to execute the work items, just call the Start() method. The same method exists in the Work Items Group for the same purpose.

    Note that if you create a suspended Work Items Group in a suspended Smart Thread Pool, starting the Work Items Group won't execute the work items until the Smart Thread Pool is started.

  16. Threads have priority.

    The STPStartInfo contains a property that defines the priority in which the threads are started in the SmartThreadPool. Use it if you know what you are doing. Playing with threads priority may end up with dead locks, live lock, and days locked :-(.

When to use?

The Smart Thread Pool is good when your work items don't do too much, but wait for events, IOs, sockets, etc. This means that the work items don't use CPU, but run for a long time. It is also good when you don't need to keep alive too many threads in the air all the time. If your work items do a short time work, then use the .NET ThreadPool. If you have a constant heavy load of work, then use Toub's thread pool and define the maximum number of threads accordingly.

How to use?

When the Smart Thread Pool or Work Items Group is created, it requires a few parameters; when a value is not provided, a default value is used.

Description Value name Default value Smart Thread Pool Work Items Group
Idle timeout IdleTimeout 60 seconds Used Not used
Maximum number of threads MaxWorkerThreads 25 Used Not used
Minimum number of threads MinWorkerThreads 0 Used Not used
Use caller thread call context UseCallerCallContext false Used Used
Use caller thread HTTP context UseCallerHttpContext false Used Used
Dispose of the state objects DisposeOfStateObjects false Used Used
Call to PostExecute CallToPostExecute CallToPostExecute.Always Used Used
PostExecute method PostExecuteWorkItemCallback null (Do nothing) Used Used
Start suspended StartSuspended false Used Used

Once defined in the construction, they cannot be changed. So, choose their values according to your needs. The minimum number of threads should be proportional to the number of work items that you want to handle at normal times. The maximum number of threads should be proportional to the number of work items that you want to handle at peak times. The idle timeout should be proportional to the peak length time.

Code examples

Creating a Smart Thread Pool instance:

SmartThreadPool smartThreadPool = 
 new SmartThreadPool(
    10*1000,    // Idle timeout in milliseconds

    25,         // Threads upper limit

    5,          // Threads lower limit

    true);      // Use caller thread context

Another way to create an instance:

// Create a STPStartInfo object

STPStartInfo stpStartInfo = new STPStartInfo();

// Change the defaults of the STPStartInfo object

stpStartInfo.DisposeOfStateObjects = true;

// Create the SmartThreadPool instance

SmartThreadPool smartThreadPool = 
        new SmartThreadPool(stpStartInfo);

Using the Smart Thread Pool:

The following snippet is a simple example. The user queues a work item and then gets the result. Note that the Result property blocks until a result is available or the work item is cancelled:

public class SimpleExample
{
    public void DoWork(object state) 
    { 
        SmartThreadPool smartThreadPool = 
                            new SmartThreadPool();

        // Queue the work item

        IWorkItemResult wir = 
        smartThreadPool.QueueWorkItem(
            new WorkItemCallback(this.DoRealWork), 
            state); 

        // Do some other work here


        // Get the result of the operation

        object result = wir.Result;

        smartThreadPool.Shutdown();
    } 

    // Do the real work 

    private object DoRealWork(object state)
    { 
        object result = null;

        // Do the real work here and put 

        // the result in 'result'


        return result;
    }
}

This example shows how you can wait for specific work items to complete. The user queues two work items, waits for both of them to complete, and then gets the results:

public class WaitForAllExample
{
    public void DoWork() 
    { 
        SmartThreadPool smartThreadPool = new SmartThreadPool();

        IWorkItemResult wir1 = 
            smartThreadPool.QueueWorkItem(new 
            WorkItemCallback(this.DoSomeWork1), null);

        IWorkItemResult wir2 = 
            smartThreadPool.QueueWorkItem(new 
            WorkItemCallback(this.DoSomeWork2), null);

        bool success = SmartThreadPool.WaitAll(
               new IWorkItemResult [] { wir1, wir2 });

        if (success)
        {
            int result1 = (int)wir1.Result;
            int result2 = (int)wir2.Result;
        }

        smartThreadPool.Shutdown();
    } 

    private object DoSomeWork1(object state)
    { 
        return 1;
    }

    private object DoSomeWork2(object state)
    { 
        return 2;
    }
}

This example shows how you can wait for one of the specific work items to complete. The user queues two work items, waits for one of them to complete, and then gets its result:

public class WaitForAnyExample
{
    public void DoWork() 
    { 
        SmartThreadPool smartThreadPool = new SmartThreadPool();

        IWorkItemResult wir1 = 
            smartThreadPool.QueueWorkItem(new 
            WorkItemCallback(this.DoSomeWork1), null);

        IWorkItemResult wir2 = 
            smartThreadPool.QueueWorkItem(new 
            WorkItemCallback(this.DoSomeWork2), null);

        IWorkItemResult [] wirs = 
                new IWorkItemResult [] { wir1, wir2 };

        int index = SmartThreadPool.WaitAny(wirs);

        if (index != WaitHandle.WaitTimeout)
        {
            int result = (int)wirs[index].Result;
        }

        smartThreadPool.Shutdown();
    } 

    private object DoSomeWork1(object state)
    {
        return 1;
    }

    private object DoSomeWork2(object state)
    { 
        return 1;
    }
}

The following example shows the use of WaitForIdle(). We just queue all the work items and then wait for all of them to complete. Note that we ignore the results of the work items:

public class WaitForIdleExample
{
    public void DoWork(object [] states) 
    { 
        SmartThreadPool smartThreadPool = new SmartThreadPool();

        foreach(object state in states)
        {
            smartThreadPool.QueueWorkItem(new 
                WorkItemCallback(this.DoSomeWork), state);
        }

        // Wait for the completion of all work items

        smartThreadPool.WaitForIdle();

        smartThreadPool.Shutdown();
    } 

    private object DoSomeWork(object state)
    { 
        // Do the work

        return null;
    }
}

The following example shows how to handle exceptions. Pay attention to the Result property that throws WorkItemResultException and not the real exception:

public class CatchExceptionExample
{
    private class DivArgs
    {
        public int x;
        public int y;
    }

    public void DoWork() 
    { 
        SmartThreadPool smartThreadPool = 
                         new SmartThreadPool();

        DivArgs divArgs = new DivArgs();
        divArgs.x = 10;
        divArgs.y = 0;

        IWorkItemResult wir = 
            smartThreadPool.QueueWorkItem(new 
                WorkItemCallback(this.DoDiv), divArgs);

        try
        {
            int result = (int)wir.Result;
        }
        // Catch the exception that Result threw

        catch (WorkItemResultException e)
        {
            // Dump the inner exception which DoDiv threw

            Debug.WriteLine(e.InnerException);
        }

        smartThreadPool.Shutdown();
    } 

    private object DoDiv(object state)
    { 
        DivArgs divArgs = (DivArgs)state;
        return (divArgs.x / divArgs.y);
    }
}

This is another example that shows how to handle exceptions. It is better than the previous one because it is faster. .NET works fast when everything is OK. When .NET needs to deal with exceptions, it becomes slower:

public class GetExceptionExample
{
    private class DivArgs
    {
        public int x;
        public int y;
    }

    public void DoWork() 
    { 
        SmartThreadPool smartThreadPool = 
                                 new SmartThreadPool();

        DivArgs divArgs = new DivArgs();
        divArgs.x = 10;
        divArgs.y = 0;

        IWorkItemResult wir = 
            smartThreadPool.QueueWorkItem(new 
                WorkItemCallback(this.DoDiv), divArgs);

        Exception e = null;
        object obj = wir.GetResult(out e);
        // e contains the expetion that DoDiv threw

        if(null == e)
        {
            int result = (int)obj;
        }
        else
        {
            // Do something with the exception

        }

        smartThreadPool.Shutdown();
    } 

    private object DoDiv(object state)
    { 
        DivArgs divArgs = (DivArgs)state;
        return (divArgs.x / divArgs.y);
    }
}

The next example shows how to create a Work Items Group and use it:

public class WorkItemsGroupExample
{
    public void DoWork(object [] states)
    { 
        SmartThreadPool smartThreadPool = new SmartThreadPool();

        // Create a work items group that processes 

        // one work item at a time

        IWorkItemsGroup wig = 
                smartThreadPool.CreateWorkItemsGroup(1);

        // Queue some work items 

        foreach(object state in states)
        {
            wig.QueueWorkItem(
                new WorkItemCallback(this.DoSomeWork), state);
        }

        // Wait for the completion of all work 

        // items in the work items group

        wig.WaitForIdle();

        smartThreadPool.Shutdown();
    } 

    private object DoSomeWork(object state)
    { 
        // Do the work

        return null;
    }
}

The next example shows how to create a suspended Smart Thread Pool:

public class SuspendedSTPStartExample
{
    public void DoWork(object [] states) 
    { 
        STPStartInfo stpStartInfo = new STPStartInfo();
        stpStartInfo.StartSuspended = true;

        SmartThreadPool smartThreadPool = 
             new SmartThreadPool(stpStartInfo);

        foreach(object state in states)
        {
            smartThreadPool.QueueWorkItem(new 
                WorkItemCallback(this.DoSomeWork), state);
        }

        // Start working on the work items in the queue

        smartThreadPool.Start();

        // Wait for the completion of all work items

        smartThreadPool.WaitForIdle();

        smartThreadPool.Shutdown();
    } 

    private object DoSomeWork(object state)
    { 
        // Do the work

        return null;
    }
}

The next example shows how to create a suspended Work Items Group:

public class SuspendedWIGStartExample
{
    public void DoWork(object [] states) 
    { 
        SmartThreadPool smartThreadPool = new SmartThreadPool();

        WIGStartInfo wigStartInfo = new WIGStartInfo();
        wigStartInfo.StartSuspended = true;

        IWorkItemsGroup wig = 
           smartThreadPool.CreateWorkItemsGroup(1, wigStartInfo);

        foreach(object state in states)
        {
            wig.QueueWorkItem(new 
                WorkItemCallback(this.DoSomeWork), state);
        }

        // Start working on the work items 

        // in the work items group queue

        wig.Start();

        // Wait for the completion of all work items

        wig.WaitForIdle();

        smartThreadPool.Shutdown();
    } 

    private object DoSomeWork(object state)
    { 
        // Do the work

        return null;
    }
}

The last example shows how to get the Work Items Group's OnIdle event:

public class OnWIGIdleEventExample
{
    public void DoWork(object [] states) 
    { 
        SmartThreadPool smartThreadPool = new SmartThreadPool();

        IWorkItemsGroup wig = 
            smartThreadPool.CreateWorkItemsGroup(1);

        wig.OnIdle += new WorkItemsGroupIdleHandler(wig_OnIdle);

        foreach(object state in states)
        {
            wig.QueueWorkItem(new 
                WorkItemCallback(this.DoSomeWork), state);
        }

        smartThreadPool.WaitForIdle();
        smartThreadPool.Shutdown();
    } 

    private object DoSomeWork(object state)
    { 
        // Do the work

        return null;
    }

    private void wig_OnIdle(IWorkItemsGroup workItemsGroup)
    {
        Debug.WriteLine("WIG is idle");
    }
}

Disclaimer

THIS CODE AND INFORMATION IS PROVIDED 'AS IS' WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralWait And GetResult Question Pin
hazem12
3:49 20 Oct '09  
GeneralRe: Wait And GetResult Question Pin
Ami Bar
4:27 20 Oct '09  
GeneralRe: Wait And GetResult Question Pin
hazem12
4:49 20 Oct '09  
AnswerRe: Wait And GetResult Question Pin
Ami Bar
4:53 20 Oct '09  
GeneralWaitAll gives an error Pin
Win32nipuh
1:54 13 Jul '09  
GeneralRe: WaitAll gives an error Pin
Ami Bar
2:37 13 Jul '09  
GeneralRe: WaitAll gives an error Pin
Win32nipuh
2:54 13 Jul '09  
QuestionMaintaining the code Pin
Gilad Kapelushnik
4:19 8 Jun '09  
AnswerRe: Maintaining the code Pin
Ami Bar
4:30 8 Jun '09  
Generalgood work you have already done so far Pin
mah_hat
4:56 8 Apr '09  
GeneralOnWorkItemComplete Pin
Glenn MacGregor
7:19 7 Apr '09  
GeneralRe: OnWorkItemComplete Pin
Ami Bar
7:23 7 Apr '09  
GeneralBefore queueing item in Pool, is it possible to see if there is the same item in the pool that hasn't ended yet? Pin
CSharpMember
6:33 18 Feb '09  
GeneralRe: Before queueing item in Pool, is it possible to see if there is the same item in the pool that hasn't ended yet? Pin
Ami Bar
22:04 18 Feb '09  
QuestionMemory Leaks [modified] Pin
Tauqir Aslam
20:53 11 Jan '09  
AnswerRe: Memory Leaks Pin
Ami Bar
20:21 12 Jan '09  
GeneralRe: Memory Leaks Pin
davidkirkland
10:17 5 Mar '09  
GeneralRe: Memory Leaks Pin
Ami Bar
10:21 5 Mar '09  
GeneralRe: Memory Leaks Pin
larisatudos
9:19 18 Mar '09  
GeneralRe: Memory Leaks Pin
beir
5:30 30 Jul '09  
GeneralRe: Memory Leaks Pin
Ami Bar
22:18 31 Jul '09  
GeneralRe: Memory Leaks Pin
Antony Baskar
0:51 25 Aug '09  
GeneralRe: Memory Leaks Pin
Antony Baskar
21:00 27 Aug '09  
Generaldelay in threads starting Pin
asdfcoder
16:00 7 Jan '09  
AnswerRe: delay in threads starting Pin
Ami Bar
23:05 7 Jan '09  


Last Updated 13 Feb 2006 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2009