Click here to Skip to main content
Click here to Skip to main content

Managed I/O Completion Ports (IOCP) - Part 2

, 26 Apr 2006
Rate this:
Please Sign up or sign in to vote.
Lock-Free Object Pool, Lock-Free Queue, and Thread Pool for Managed IOCP.

Index

  1. Introduction
  2. Managed IOCP with Lock-Free Queue
  3. Lock-Free Object Pool
  4. Using Lock-Free Object Pool in .NET applications
  5. Putting it all together - Design Overview of ManagedIOCP
  6. The ManagedIOCP Thread Pool
  7. Inside the ManagedIOCP Thread Pool
  8. Extensible Task Framework for the ManagedIOCP Thread Pool
  9. Sonic.Net source and demo applications
  10. Points of interest
  11. History
  12. Software usage

1. Introduction

Managed I/O Completion Ports (IOCP) is part of a .NET class library named Sonic.Net, which I first released on CodeProject in May 2005. Sonic.Net is a free open source class library that can be used in building highly scalable server side .NET applications. This part-2 of Managed IOCP builds on top of my first Managed IOCP article. So it is a pre-requisite to read the first part of Managed IOCP before you read this part two of Managed IOCP. My first article on Managed IOCP is titled "Managed I/O Completion Ports (IOCP)". I request the readers to read the discussion threads in my first article, as they have a wealth of clarifications and information regarding Managed IOCP and its usage. Especially, the discussion with 'Craig Neuwirt' has brought out a critical issue with my original Managed IOCP implementation and helped me rectify it and make ManagedIOCP better (thanks Craig Smile | :) ).

2. Managed IOCP with Lock-Free Queue

Before getting into Lock-Free stuff, the Managed IOCP source and demo applications contained in the downloadable Zip of this article are compiled using my Lock-Free Queue class. Removing the conditional compiler constant LOCK_FREE_QUEUE from Sonic.Net project properties will enable the Sonic.Net assembly to be compiled with the .NET synchronized Queue class.

Coming back to our Lock-Free stuff, I used a synchronized System.Collection.Queue class as the internal object queue for Managed IOCP in the Sonic.Net v1.0 class library. This provides a scope for lock contention between threads that are dispatching objects to Managed IOCP, and also between threads that are retrieving objects from Managed IOCP. I had to use a synchronized queue because the pop and push operations of a queue are not atomic. During a push operation, a thread has to first set the next node of the queue's tail node to point to the new node that holds the object to be enqueued. Next, the thread has to point the tail to the new node as the new node will now be the tail of the queue. These two operations cannot be performed atomically (with a single CPU instruction). This means that when multiple-threads are pushing objects (enqueuing) to the same instance of a queue, there are chances that some of the enqueue operations will be unsuccessful, and more dangerously, without the thread performing the push operation ever coming to know about it. Here is the scenario...

  1. Thread-A started the enqueue (push) operation.
  2. Thread-A created new node that holds the new object to be enqueued.
  3. Thread-A assigned the new node address to the current tail's next node.
  4. Thread-B started the enqueue (push) operation.
  5. Thread-B created the new node that holds the new object to be enqueued.
  6. Thread-B assigned the new node address to the current tail's next node.

Here comes the disaster. Thread-A thought that it has pointed the current tail's next node to its new node. But before it could point the tail to the new node (to make the new node the new tail), Thread-B has pointed the current tail's next node to its own new node. Oops!!! Thread-A's new node future is now hanging in mid air. The disaster is not yet over. Check out the following sequence of operations in continuation with the above mentioned operations of Thread-A and Thread-B.

  1. Thread-A pointed the current tail to its new node (hoping to make its new node the new tail).
  2. Thread-A exited out of the enqueue (push) operation.
  3. Thread-B pointed the current tail to its new node, and in-fact, made its new node the new tail.

After step-9 in the above sequence of events, the new node that is actually enqueued by Thread-A is lost. It is lost because neither the tail's next node nor the tail is pointing to it. It has become an orphaned object. In the C++ world, it would have led to serious memory leaks in the application as there is no way to reach the new node and its contained object, so no one will be able to free it. Fortunately, in the .NET world, the CLR's Garbage Collector will come to our rescue, and will eventually cleanup the orphaned node and its contained object later at some point of time. But the more serious problem here is not with the orphaned object but with the lost enqueue operation without the knowledge of the thread (Thread-A) that is performing the operation. Thread-A would think it has enqueued the object successfully, and would not report any error. This leads to missing objects, which could mean loss of data in the application. Loss of data, that too without notice, is a serious problem for any application.

It should be clear from the above discussion that the following two logically related operations should be synchronized using some locking mechanism, so that only one thread in the process could execute them at any given point of time.

  1. Thread-X assigned the new node address to the current tail's next node.
  2. Thread-X pointed the current tail to its new node and made its new node the new tail.

Synchronizing access to common objects using locking may not always be the right approach. It will definitely stop the loss of data and corruption, but when your application is running in a multi-processor environment, it may reduce the performance of your application. This is because in a multi-processor environment, you will experience true parallelism in thread execution. So, when two threads are running in parallel and trying to enqueue (push) objects onto the same queue, then one of the threads has to wait until the other has come out of the locked code region. Since this enqueue operation is a core function that is used very frequently in Managed IOCP based applications, the threads _may_ experience lock contention, and the kernel activity might go up significantly as the OS has to keep switching the thread from running to suspended mode and vice-versa. It is desirable to suspend the thread when there are no objects queued onto the Managed IOCP. But when objects are present, it would be good if each thread is active for the maximum time possible in its given CPU cycle time.

We (Windows developers) have been using lock based synchronization successfully in Windows environment till now. So we can continue using it. But there are other more promising alternative techniques to lock based synchronization, especially when designing highly scalable server side applications. These are called Lock-Free algorithms. These techniques will allow multiple threads to safely use common objects without corrupting the object state or causing any loss of data. These techniques have been under heavy research in the recent past, and are making their way slowly into mainstream application development. The reason of their slower adoption is, it is difficult to prove the correctness of these techniques while implementing on certain data-structures like hashtables. But fortunately, for the Managed IOCP queue that is used heavily by multiple-threads, there are matured and well tested lock-free algorithms.

2.1. Lock-Free Queue

The Lock-Free queue that I implemented to use in Managed IOCP is built on one of the matured algorithms for designing the Lock-Free queue data structure. As we discussed earlier, we need two physical operations to perform a single enqueue (push) operation on a queue. So what we need to check is that, while we read the then next node of the current tail and assign our new node to it, no other thread should have modified the next node of the current tail. So, the read and exchange of the next node of the current tail should either succeed or fail atomically. The synchronization primitive provided by .NET (supported by hardware and Windows OS), CAS, is a perfect fit for this situation. I discussed about CAS in detail in section 4.2 of my first article on Managed IOCP (read section 1 for details on links to my first article on Managed IOCP).

Also, the loss of data that I discussed in this section is solved using a trailing tail technique. In this technique, after step-3 in above discussed sequence, the current tail's next node will be pointing to the new node created by Thread-A. So when Thread-B starts its enqueue operation, it can check if the current tail's next node is null. If it is not, it means that some other thread has changed the current tail's next node, but it has not yet modified the tail itself. So Thread-B would advance the _trailing_ current tail to point to the tail's own next node as Thread-A would anyway do it when it is given a CPU time slice by the OS. Now, Thread-B could restart its enqueue operation. When Thread-B successfully changes the current tail's next node, it can break out of this next node checking loop and point the tail to its new node, provided some other thread has not already advanced the tail during the next node checking step in the enqueue operation.

The code below demonstrates this trailing tail technique used to achieve lock-free enqueue operation:

public void Enqueue(object data)
{
    Node tempTail = null;
    Node tempTailNext = null;
    Node newNode = _nodePool.GetObject() as Node; //new Node(data);
    newNode.Data = data;
    do
    {
        tempTail = _tail as Node;
        tempTailNext = tempTail.NextNode as Node;
        if (tempTail == _tail)
        {
            if (tempTailNext == null)
            {
                // If the tail node we are referring to is really the last
                // node in the queue (i.e. its next node is null), then
                // try to point its next node to our new node
                //
                if (Interlocked.CompareExchange(ref tempTail.NextNode, 
                           newNode,tempTailNext) == tempTailNext)
                    break;
            }
            else
            {
                // This condition occurs when we have failed to update
                // the tail's next node. And the next time we try to update
                // the next node, the next node is pointing to a new node
                // updated by other thread. But the other thread has not yet
                // re-pointed the tail to its new node.
                // So we try to re-point to the tail node to the next node of the
                // current tail
                //
                Interlocked.CompareExchange(ref _tail,tempTailNext,tempTail);
            }
        }
    } while (true);

    // If we were able to successfully change
    // the next node of the current tail node
    // to point to our new node, then re-point
    // the tail node also to our new node
    //
    Interlocked.CompareExchange(ref _tail,newNode,tempTail);
    Interlocked.Increment(ref _count);
}

One interesting point to note in the above code is that I used CAS (Interlocked.CompareExchange) on object types. This is a beautiful feature supported by .NET. This form of CAS will compare the object reference value pointed to by the first parameter, with that of the comparand, which is the third parameter, and will make the first variable point to the object specified in the second parameter. The downside of the current Interlocked.CompareExchange on object types is that you cannot use variables of your own reference types with this .NET API. So, I had to use the object data type to define my data members in the Node class and for the head and tail object references in the ManagedIOCP class. As you can observe in the above code for the Enqueue method, this leads to type casting of variables from the object to the Node type.

You can check out the lock-free Dequeue operation in the source code. After reading the above discussion, it would be easy to understand the lock-free Dequeue (pop) operation. I have also provided code comments to help the reader understand the logic behind the code.

2.2. Test results of Managed IOCP with Lock-Free Queue

I have run the same WinForms based demo application using Managed IOCP compiled with the Lock-Free queue implementation on a Pentium IV 3.0 GHz HT system and a Pentium III 700 MHz single processor system. The results are slightly better compared to the .NET synchronized Queue. I noticed good reduction in lock contention in the console demo application provided with this article, when using Managed IOCP with the Lock-Free queue (using the Performance Monitor application) when compared to using Managed IOCP with the .NET synchronized Queue class. I did not see noticeable benefits in terms of speed. This may be because of the fact that my demo applications are just used as a testing bed for verifying the feature correctness of Managed IOCP and other Sonic.Net classes, and stress testing them to identify any hidden multi-threading bugs. I'm positive that when used in real application scenarios with good processing in the threads, Managed IOCP with Lock-Free queue should perform better.

I believe that despite all the optimizations I discussed regarding the lock-free queue, the performance gained by using it varies based on its usage environment. So I request developers using Managed IOCP to test their application using both the .NET synchronized Queue and my custom implemented Lock-Free queue, to get a feel of the performance of both the queue classes.

3. Lock-Free Object Pool

Object Pooling is a technique that allows us to re-use existing objects again and again with different states. For example, in the Lock-Free queue data structure, I have a Node object that represents a node in the queue. But the only usage of the Node object is to hold the objects that are pushed onto the queue. After an object is popped out of the queue, the Node object holding it has no active references and will be garbage collected by the CLR at some point of time.

It would be efficient if I can re-use the Node object for other push operations, after its contained object is popped out of the queue. This way, I can reduce a lot of new object allocations, thus reducing the GC activity within the application. This will also increase the overall performance of the application as there would be less number of objects to garbage collect and the CLR will not hinder the execution of the application frequently for garbage collection.

But to maintain the list of freed-up Node objects, we need a data structure that can maintain a queue, which does not require new Node allocations. This special queue would act like a linked list, where we can insert new links at the top and remove links from the bottom (FIFO). This queue is special because when an object, in our case Node, is pushed onto it, it does not create a new Node to hold it. It assumes the objects pushed onto it contain a link member that can be used to link up the next Node in the queue. With this assumption, it can use the objects pushed onto it as Nodes by themselves. For this purpose, I created a type called "PoolableObject" that has a data member that points to an object of the same type (PoolableObject). So if I push an object of "PoolableObject" onto our Object Pool queue, it will just point our new object's link data member to the current top element of the queue, thus making our new object the top of the queue. When you pop out the object from this queue, it will just return the bottom object of the queue, and will set the link data member of the previous element of the bottom element to null, thus making it the bottom most element ready to be popped out.

The code below shows the definition of the "PoolableObject" type. Any type whose objects need to be pooled can derive from this type.

/// <summary>
/// Poolable object type. One can define new poolable types by deriving
/// from this class.
/// </summary>
public class PoolableObject
{
    /// <summary>
    /// Default constructor. Poolable types need to have a no-argument
    /// constructor for the poolable object factory to easily create
    /// new poolable objects when required.
    /// </summary>
    public PoolableObject()
    {
        Initialize();
    }
    /// <summary>
    /// Called when a poolable object is being returned from the pool
    /// to caller.
    /// </summary>
    public virtual void Initialize()
    {
        LinkedObject = null;
    }
    /// <summary>
    /// Called when a poolable object is being returned back to the pool.
    /// </summary>
    public virtual void UnInitialize()
    {
        LinkedObject = null;
    }
    internal object LinkedObject;
}

The above mentioned PoolableObject type is used by a class called ObjectPool that provides us with a mechanism to store objects in a pool. This class provides us with methods to add new objects to the pool and retrieve existing objects from the pool. This ObjectPool class is implemented using the special FIFO Lock-Free queue that we talked about in the beginning of this section. This queue does not allocate Nodes to hold the poolable objects, rather it uses the object queued onto it as a Node by itself. For this reason, we said that we need a type that provides us with the ability to link itself to an object of the same type. Here comes the "PoolableObject" type defined in the above code snippet.

The above definition of PooledObject is simple. It has a default no-argument constructor for easy creation of poolable objects by a factory class named PoolableObjectFactory (I'll explain this in a moment). It has a virtual Initialize method that can be overridden by the derived classes to perform any initialization. This method is called by ObjectPool class when a new poolable object is created by the poolable object factory and when the ObjectPool class is returning an object from its object pool queue. It has a virtual UnInitialize method that can be overridden by the derived classes to perform any un-initialization. This method is called by ObjectPool when a poolable object is being added to its object pool queue.

As I mentioned above, I'm using a factory class, PoolableObjectFactory, to create new objects that are derived from the PoolableObject type. I need this because initially when a new ObjectPool is created, there will not be any objects in its pool. So when an application asks it for an object from its pool, it silently creates a new object and will return it to the caller. But for the ObjectPool to create a new poolable object, it does not know about the type of the object that is derived from the PoolableObject. This is application specific, and only the application that created a new type from the PoolableObject type knows what type of objects it will pool using the ObjectPool class. So, I provided an abstract factory type called PoolableObjectFactory, which can be implemented by developers to create and return the objects of their poolable type. As mentioned above in this section, the poolable types that the developers create should be derived from the abstract poolable type "PoolableObject".

The code below shows the definition of the PoolableObjectFactory type:

/// <summary>
/// Defines a factory interface to be implemented by classes
/// that creates new poolable objects
/// </summary>
public abstract class PoolableObjectFactory
{
    /// <summary>
    /// Create a new instance of a poolable object
    /// </summary>
    /// <returns>Instance of user defined
    ///    PoolableObject derived type</returns>
    public abstract PoolableObject CreatePoolableObject();
}

4. Using Lock-Free Object Pool in .NET applications

The above section (section 3) discussed most of the details about PoolableObject and PoolableObjectFactory. In this section, I'll show you a practical implementation of the PoolableObject and PoolableObjectFactory types. This section will help you build your own poolable objects in your .NET applications that can be managed and pooled for you by the ObjectPool class.

As I mentioned in my previous section (section 3), the Node type used by the Lock-Free queue is a poolable object. It is derived from the PoolableObject type. It also shows a simple implementation of the Initialize method of the PoolableObject type by derived classes. The code below shows the definition of the Node type.

/// <summary>
/// Internal class used by all other data structures
/// </summary>
class Node : PoolableObject
{
    public Node()
    {
        Init(null);
    }
    public Node(object data)
    {
        Init(data);
    }
    public override void Initialize()
    {
        Init(null);
    }
    private void Init(object data)
    {
        Data = data;
        NextNode = null;
    }
    public object Data;
    public object NextNode;
}

In the above definition of the Node class, you can see the overridden method Initialize of the PoolableObject class in bold. I did not override the PoolabelObject::UnInitialize method as the Node class need not do any un-initialization work. Instead, the default one provided by the PoolableObject class is sufficient. Remember that the derived class implementation of UnInitialize should call the base class (PoolableObject) UnInitialize method, as it does an important job of setting its Link data member to null. This is important for the proper functioning of the ObjectPool class.

Once we define a new poolable type, we need to provide an implementation of the abstract PoolableObjectFactory that will be used by the ObjectPool class to create new poolable objects when required. The code below shows an implementation of the PoolableObjectFactory class that I used to create new instances of the poolable Node type.

/// <summary>
/// Factory class to create new instances of the Node type
/// </summary>
class NodePoolFactory : PoolableObjectFactory
{
    /// <summary>
    /// Creates a new instance of poolable Node type
    /// </summary>
    /// <returns>New poolable Node object</returns>
    public override PoolableObject CreatePoolableObject()
    {
        return new Node();
    }
}

Now we have a type (Node) that is poolable using the ObjectPool class, and a type (NodePoolFactory) that can be used by ObjectPool to create new instances of our poolable Node type when required. Now, to use poolable Node objects, it is as simple as creating an instance of the ObjectPool class and providing it a reference to an instance of the NodePoolFactory class. Below is a code snippet showing the creation of a ObjectPool instance, taken from the Lock-Free queue class.

private ObjectPool _nodePool = 
         new ObjectPool(new NodePoolFactory());

Once the ObjectPool is instantiated, we can get an object of our poolable object type (Node) from the pool by using the GetObject() instance method of the ObjectPool class. The code below shows how to use the ObjectPool class to get the objects from the pool.

Node newNode = _nodePool.GetObject() as Node;

When we are done with a poolable object, we should give it back to the pool so that it can be re-used later. We can add an object back to the pool by calling the AddToPool() instance method on the ObjectPool class. For instance, in the Lock-Free queue class, once an object is popped out of the Queue, the Node holding that object can be re-used to hold any new object to be enqueued onto the Queue. So, just before leaving the Dequeue operation, we add the Node object to the Node object pool maintained by the Queue class.

_nodePool.AddToPool(tempHead);

5. Putting it all together - Design Overview of ManagedIOCP

The above diagram shows the design of ManagedIOCP and its relation to the Lock-Free ObjectPool and the Lock-Free Queue.

6. The ManagedIOCP Thread Pool

Thread Pools have been an integral part of applications with a good amount of asynchronous and parallel computing requirements. Generally, server side applications use Thread Pool for a consistent and easy to use programming model for executing tasks in parallel and asynchronously. With Managed IOCP as the core technology, I built a Thread Pool that not only provides basic thread management but few other important features as listed below:

  1. Maximum allowed threads in the Thread Pool (different from concurrency limit).
  2. Concurrency limit on active threads in the Thread Pool.
  3. Extensible Task framework for defining application tasks that can be executed by the Thread Pool.

Before getting into the inside implementation of the ManagedIOCP Thread Pool, I'll describe its usage in a .NET application. Firstly, ManagedIOCP executes objects that implement an interface named ITask. The definition of the ITask interface is shown below:

/// <summary>
/// Interface used by ThreadPool class for executing
/// Tasks dispatched to it
/// </summary>
public interface ITask
{
    /// <summary>
    /// Executes the corresponding task
    /// </summary>
    /// <param name="tp">ThreadPool onto which
    ///        this Task is dispatched</param>
    void Execute(ThreadPool tp);
    /// <summary>
    /// Specifies to the ThreadPool whether to Execute this task based on whether
    /// this Task is Active or not. This allows for cancellation of Tasks after
    /// they are dispatched to ThreadPool for execution
    /// </summary>
    bool Active {get;set;}
    /// <summary>
    /// Indicates the task that its execution has been completed.
    /// </summary>
    void Done();
}

As shown from the above definition, the Execute method is where the logic for executing the task should be written. Once you have a type implementing the ITask interface with logic in its Execute method, using the ManagedIOCP Thread Pool is as simple as creating an instance of it and dispatching the ITask objects onto it. When a ITask object is chosen by the Thread Pool for execution, it will call the Execute method on the object. The code below shows a dummy implementation of the ITask interface and how to use it with the ManagedIOCP Thread Pool.

public class MyTask : ITask
{
    #region ITask Members

    public void Execute(Sonic.Net.ThreadPool tp)
    {
        // Do Some Processing
        // TODO::

        // Dispatch more objects to Thread Pool if required
        MyTask objTsk = new MyTask();
        tp.Dispatch(objTsk);
    }

    public void Done()
    {
        // May be you can pool this object, so that it can
        // be re-used
        // TODO::
    }

    public bool Active
    {
        get
        {
            return _active;
        }
        set
        {
            _active = value;
        }
    }

    #endregion
    
    // By default one can choose the ITask object to be active
    // or not. In this sample I chose it to be active by default
    private bool _active = true;
}

The above code shows a type MyTask that implements the ITask interface. If you observe the code, the Execute method has a ThreadPool object as parameter, so that the object being executed by the Thread Pool has access to the ThreadPool object itself for any further dispatching of objects onto the Thread Pool that is executing the current task object.

Also, the ITask interface has two other important members. The Done method is called by the Thread Pool, when the Execute method on a ITask object is completed. This gives a chance to the ITask object to perform any clean-up or pool itself for re-use (this is a powerful concept, and I'll discuss this shortly in the 'Task Framework' section). The Active property indicates the Thread Pool whether to execute this ITask object (whether to call the Execute method) or not. So if an application, after dispatching an ITask object to the Thread Pool, for some reason decides to cancel the task execution, it can set the task object's Active property to false, thus canceling the task execution, provided the task object has not already been executed by the Thread Pool.

The code below shows how to create an instance of the Thread Pool in the first place, and dispatch a MyTask object to it for asynchronous and parallel execution:

// Create a new instance of the ManagedIOCP Thread Pool class
// with 10 maximum threads and 5 concurrent active threads
ThreadPool tp = new ThreadPool(10,5);

// Create a new new instance of MyTask object and dispatch it
// to the Thread Pool for asynchronous and parallel execution
ITask objTask = new MyTask();
tp.Dispatch(objTask);

7. Inside the ManagedIOCP Thread Pool

The ManagedIOCP Thread Pool is implemented as a simple wrapper around the core ManagedIOCP class. When an instance of a ManagedIOCP Thread Pool is created, it internally creates an instance of ManagedIOCP, creates all the maximum number of threads, and will register those threads with the ManagedIOCP instance. When a thread of the Thread Pool retrieves an object from the ManagedIOCP instance of the Thread Pool, it will cast the object to a ITask object and will call the Execute method on it.

If you observe the constructor of the ThreadPool class, it has a second form of constructor that takes in a delegate named ThreadPoolThreadExceptionHandler. When a handler is provided for this parameter, if a Thread Pool thread encounters any exceptions while executing the ITask object, it will call this delegate and will continue processing other objects. In case the handler throws any exception, the exception is ignored. If no handler is provided for this delegate, then the thread will ignore the exception and will still continue processing other objects.

7.1. Managing Burst and Idle situations in ManagedIOCP ThreadPool

Creating all the maximum threads at once for each Thread Pool instance will not be an overhead on the system. Because, the number of active threads in a ManagedIOCP Thread Pool is controlled by the concurrency limit of the Thread Pool, which is specified during the instantiation of a Thread Pool instance and which can also be set at runtime. There could be situations where the active number of threads retrieve ITask objects from the ManagedIOCP instance of the Thread Pool, and while processing them, might go into wait mode (not-running). This could happen if the Execute method of the ITask object is calling into a Web-Service synchronously, etc. When this happens, if there are any pending ITask objects in its queue, the ManagedIOCP instance of the Thread Pool will wake-up other sleeping threads for processing those objects. While these extra threads are processing the ITask object, the earlier threads that went into sleeping mode while executing their ITask objects could come out of sleeping mode and start running. This will create a state in the Thread Pool where more than the allowed concurrent number of threads will be running at a given point of time.

This above discussed situation can be eliminated by having the max. number of threads in the Thread Pool equivalent to the number of allowed concurrent threads. But this may reduce the scalability of the application. This is because if all running threads are waiting on external resource/triggers/events like web service calls, though the application is idle, it will not be able to service any pending requests.

Having the max. number of threads greater than the allowed concurrent threads in the Thread Pool is always desired to scale the application under loads and utilize the system resources as much as possible and as long as possible. In order to balance out the burst situations and the idle situations, ManagedIOCP used by ThreadPool has built in support for suspending un-wanted IOCPHandles that are registered with it. When a IOCPHandle is coming into wait state, if the number of current active threads are greater than or equal to the number of allowed concurrent threads, then the IOCPHandle is queued onto a suspended queue. This way though the number of max. threads in the ThreadPool is greater than the allowed concurrent threads, the threads that are waiting for processing the requests would be closer to the allowed concurrent threads. This would not stop the actual active threads being greater than the allowed concurrent threads, but would keep the difference at minimal levels. When a new object is dispatched to ManagedIOCP, if the current active thread count is less than the allowed concurrent thread count _and_ if the registered IOCPHandle count is greater than or equal to the allowed concurrent thread count, the ManagedIOCP will try to get a suspended thread (IOCPHandle). If it finds one, the thread is chosen for handling the dispatch by setting its IOCPHandle's wait event. This situation may occur if there are a few objects to be dispatched than the allowed concurrent threads _or_ some of the active threads went into waiting mode. In either case, waking up any unsuspended thread may not be an overhead, and by all means should be able to handle the idle situation discussed in this section.

This way Dynamic ManagedIOCP should be able to handle both burst and idle situations that are common in IOCP based ThreadPool designs. Dynamic ManagedIOCP is not enabled by default in the Sonic.Net library. The code related to Dynamic ManagedIOCP is inside the conditional compilation constant DYNAMIC_IOCP. The Dynamic ManagedIOCP can be enabled by specifying the conditional compilation constant named DYNAMIC_IOCP in the Sonic.Net class library project properties.

8. Extensible Task Framework for the ManagedIOCP Thread Pool

Task Framework provides an extensible framework for creating tasks that are to be executed by the ManagedIOCP Thread Pool. It provides abstract base classes with implementation for the Active property and the Done method of the ITask interface. These abstract base classes provide different varieties of tasks, like, waitable task, context bound task and waitable context bound task. Also, each abstract task class is derived from the PoolableObject type, thus providing task pooling. Each abstract task type has an associated abstract factory class to create instances of the corresponding task type. These abstract task factory types maintain a pool of task objects.

All the abstract task classes in the Task Framework are derived from a single abstract base class named Task. This abstract base class implements the Active property and the Done method of the ITask interface and is the one that implements the PoolableObject abstract class. Other abstract task classes derive from this class and provide their own capabilities like waiting on task completion, context binding, etc. All the abstract factory classes for creating different classes of task objects are derived from a single abstract base class named TaskFactory. This abstract base class provides task object pooling. This abstract base class is in-turn derived from PoolableObjectFactory, whose abstract methods have to be implemented by applications that wish to use the Task Framework.

The diagram below shows the ManagedIOCP ThreadPool Task framework:

ManagedIOCP ThreadPool Task Frameowrk

The diagram below shows the ManagedIOCP ThreadPool Task Factory framework:

ManagedIOCP ThreadPool Task Factory Frameowrk

8.1. Creating and using Generic Task

GenericTask abstract class provides a basic implementation of the ITask interface for a task to be executed by the Thread Pool. GenericTask abstract class is derived from the Task abstract class, so that it provides features like canceling the task execution by setting the Active property value to 'false'. GenericTask is an abstract class because it does not implement the Execute method of the ITask interface. It is upto the application using the GenericTask to derive from it and implement the Execute method as required. The code below shows a class that is derived from GenericTask and implements the Execute method:

public class MyGenericTask : GenericTask
{
    public override void Execute(ThreadPool tp)
    {
        // Task execution code goes here
        // TODO::
    }
}

Once we have the application specific generic task class, we can create an instance of it and dispatch it to the ThreadPool.

MyGenericTask gt = new MyGenericTask();
// tp is an instance of ManagedIOCP ThreadPool class
tp.Dispatch(gt);

We can use TaskFactory and its derived abstract classes to create/acquire instances of the GenericTask class. The advantage is that these abstract factory classes provide object pooling of task objects. The code below shows a class that is derived from GenerictaskFactory and implements the GetObject method. This factory creates/acquires instances of an application specific GenericTask class.

class MyGenericTaskFactory : GenericTaskFactory
{
    public override PoolableObject CreatePoolableObject()
    {
        return new MyGenericTask();
    }
}

Once we have the application specific GenericTask class and associated GenericTaskFactory class, we can create/acquire instances of the application specific GenericTask class and dispatch them to the ThreadPool.

MyGenericTaskFactory gtf = new MyGenericTaskFactory();
MyGenericTask gt = gtf.NewGenericTask(null, null);
// tp is an instance of ManagedIOCP ThreadPool class
tp.Dispatch(gt);

The first null parameter passed to the NewGenericTask method of the GenericTaskFactory class is the ID given to the task. This can be set to a valid non-object for uniquely identifying the task. The second null parameter is any application related object that needs to be associated with the task. This can be used to pass additional information associated with the task, which can be used during its execution.

8.2. Creating and using Waitable Task

The WaitableTask abstract class provides a task on which the application can wait for a task to be executed by the Thread Pool, after dispatching the task to the Thread Pool. The creation and usage of WaitableTask class is same as that of GenericTask. WaitableTask does not have its own factory class, as it is an extension of GenericTask and provides a waitable mechanism to wait on the completion of the underlying GenericTask. Wait on the WaitableGenericTask supports time-out in milliseconds. If time-out occurs during wait operation, the Wait method on the WaitableGenericTask class will return 'false'. The code below shows how one can wait on the WaitableGenericTask:

MyGenericTaskFactory gtf = new MyGenericTaskFactory();
MyWaitableGenericTask gt = gtf.NewGenericTask(null, null);
// tp is an instance of ManagedIOCP ThreadPool class
tp.Dispatch(gt);
// Wait infinitely on the task to complete
bool bTimeOut = gt.Wait(-1);

8.3. Creating and using ContextBound Task

The ContextBoundGenericTask abstract class provides a task whose execution is serialized with other tasks within the same context. Context provides a logical locking/unlocking mechanism, which can be used by tasks executing under a context. When a task locks the associated context during execution, other tasks trying to lock the context will be suspended until the task that locked the context unlocks it. The ManagedIOCP Task Framework has an interface named IContext that represents a context. The Task Framework also has a default implementation of the IContext interface named Context. The Context class provides locking and unlocking semantics using the Monitor synchronization object.

The creation and usage of ContextBoundGenericTask class is same as that of the GenericTask. A separate factory class named ContextBoundGenericTaskFactory is provided with the Task Framework. Applications have to derive from the ContextBoundGenericTaskFactory class and implement its CreatePoolableObject method to create/acquire instances of classes derived from the ContextBoundGenericTask class.

One additional step in creating the ContextBoundGenericTask derived class is that, the code implemented in the Execute method of the derived class should lock and unlock the context object available in the base ContextBoundGenericClass as a property named Context.

The code below shows how to create and use an application specific ContextBoundGenericTask class:

// Application specific ContextBoundGenericTask class
public class MyContextBoundGenericTask : ContextBoundGenericTask
{
    public override void Execute(ThreadPool tp)
    {
        Context.Lock();
        // Task execution code goes here
        // TODO::
        Context.UnLock();
    }
}
// Application specific ContextBoundGenericTaskFactory class
public class MyContextBoundGenericTaskFactory : 
                ContextBoundGenericTaskFactory
{
    public override PoolableObject CreatePoolableObject()
    {
        return new MyContextBoundGenericTask();
    }
}
// Create an instance of the task factory
MyContextBoundGenericTaskFactory ctxGTF = 
          new MyContextBoundGenericTaskFactory();

// Create a new Context. Each context
// object may have a unique id, which can be
// retrieved using a context id generator
// singleton class provided by Task framework
object ctxId = 
  ContextIdGenerator.GetInstance().GetNextContextId();
Context ctx = new Context(ctxId);

// Create/Acquire an instance of application
// specific ContextBoundGenericTask object
// and associate the context with it
MyContextBoundGenericTask ctxGT = 
  ctxGTF.NewContextBoundGenericTask(null, null,ctx);

// Dispatch the task to ThreadPool for execution.
// tp is an instance of ManagedIOCP ThreadPool class
tp.Dispatch(ctxGT);

9. Sonic.Net source and demo applications

Below are the details of files included in the article's ZIP file:

  1. Sonic.Net (Folder) - I named this class library as Sonic.Net (Sonic stands for speed). The namespace is also specified as Sonic.Net. All the classes that I described in this article are defined within this namespace. The folder hierarchy is described below:
    Sonic.Net
    |
    --> Assemblies
    |
    --> Solution Files
    |
    --> Sonic.Net
    |
    --> Sonic.Net Console Demo
    |
    --> Sonic.Net Demo Application

    The Assemblies folder contains the Sonic.Net.dll (contains ObjectPool, Queue, ManagedIOCP, IOCPHandle and ThreadPool classes), Sonic.Net Demo Application.exe (demo application showing the usage of ManagedIOCP and IOCPHandle classes) and Sonic.Net Console Demo.exe (console demo application showing the usage of the ThreadPool and ObjectPool classes).

    The Solution Files folder contains the VS.NET 2003 solution file for the Sonic.Net assembly project, Sonic.Net demo application WinForms project, and Sonic.Net console demo project.

    The Sonic.Net folder contains the Sonic.Net assembly source code.

    The Sonic.Net Console Demo folder contains the Sonic.Net console demo application source code. This demo uses a file that will be read by the ThreadPool threads. Please change the file path to a valid one on your system. The code below shows the portion in code to change. This code is in the ManagedIOCPConsoleDemo.cs file.

    public static void ReadData()
    {
        StreamReader sr = 
          File.OpenText(@"C:\aditya\downloads\lgslides.pdf");
        string st = sr.ReadToEnd();
        st = null;
        sr.Close();
        Thread.Sleep(100);
    }

    The Sonic.Net Demo Application folder contains the Sonic.Net demo application source code.

  2. Win32IOCPDemo (Folder) - This folder contains the WinForms based demo application for demonstrating the Win32 IOCP usage using PInvoke. When compiled, the Win32IOCPDemo.exe will be created in the Win32IOCPDemo\bin\debug or Win32IOCPDemo\bin\Release folder based on the current build configuration you selected. Default build configuration is set to Release mode.

10. Points of interest

To summarize, we now have a Sonic.Net library that provides Lock-Free data structures like Queue and ObjectPool, asynchronous and parallel programming infrastructure classes like ManagedIOCP, ThreadPool and a Task Framework. Apart from these classes, there is a small utilities class named StopWatch that comes with the Sonic.Net assembly. The StopWatch class can be used to measure the elapsed time easily in a convenient manner. Check it out in case you are interested. Apart from the test applications I have provided with this class library, I believe that the real test for this type of class library is a good real-world server side application. I request users of this class library to provide any feedback/suggestions to fix bugs and improve it.

I'm working on a version of Sonic.Net for .NET 2.0. I'm moving Managed IOCP to a _Generic_ class with the data to be queued, defined as a template parameter. In this context, I had to use my lock-free queue for queuing objects onto Managed IOCP, as .NET 2.0 does not yet support synchronized Generic collections. I'm creating a lock-free Generic (templated) queue to be used in generic Managed IOCP. As soon as I complete, I will update this article with new code and share my experience on using .NET 2.0 Generics. Also the most exciting part of .NET 2.0 is that it supports Generic (templated) Interlocked.CompareExchange. This means the data members in our Node class and head and tail node object references in our Managed IOCP class can be of Node type rather than object type. This would be efficient and would save some type casting overhead during runtime.

11. History

Date: Apr 17, 2006

Fixed an issue related to usage of Interlocked.CompareExchange. Thanks to Smith Cameron (LexisNexis organization) for pointing out this issue.

Date: Aug 15, 2005

Sonic.Net v1.1 - Lock-Free Queue, ObjectPool, ManagedIOCP with revamped (and enhanced) thread choosing algorithm for executing dispatched objects, ManagedIOCP based ThreadPool, and an extensible Task Framework for defining tasks to be executed by the ManagedIOCP ThreadPool.

Date: May 09, 2005

I also fixed a small bug in the Windows demo application (that existed in version 1.0). This bug can allow two threads in the _demo_ application to use the same Label object to display their count. This is fixed in this version (1.1). The bug is fixed in the ManagedIOCPTestForm::StartCmd_Click(...) method.

Date: May 04, 2005

Sonic.Net v1.0 (Class library hosting ManagedIOCP and IOCPHandle class implementations with .NET synchronized Queue for holding data objects in Managed IOCP).

12. Software Usage

This software is provided "as is" with no expressed or implied warranty. I accept no liability for any type of damage or loss that this software may cause.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

P.Adityanand
Architect
India India
Software Professional with 14+ Years of experience in design & development of server products using Microsoft Technologies.
 
Woked/Working on server side product development using Managed C++ & C#, including Thread pools, Asynchronous Procedure Calls (APC), Inter Process Communication (IPC) using named pipes, Lock Free data structures in C++ & .Net, etc.

Comments and Discussions

 
Generalupdates PinmemberRafatTariq4-Dec-05 18:55 
GeneralRe: updates PinmemberP.Adityanand5-Dec-05 17:29 
GeneralRe: updates PinmemberRafatTariq5-Dec-05 18:14 
GeneralRe: updates PinmemberP.Adityanand5-Dec-05 19:08 
GeneralRe: updates PinmemberRafatTariq19-Dec-05 0:08 
GeneralRe: updates PinmemberRafatTariq19-Dec-05 0:17 
GeneralRe: updates PinmemberRafatTariq19-Dec-05 1:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141220.1 | Last Updated 26 Apr 2006
Article Copyright 2005 by P.Adityanand
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid