Click here to Skip to main content
15,880,543 members
Articles / Desktop Programming / MFC
Article

Handle Management - Complete Solution

Rate me:
Please Sign up or sign in to vote.
4.43/5 (8 votes)
6 May 2002CPOL27 min read 86.7K   1.1K   41   6
A utility class to manage the mapping of objects to handles.

Introduction

Handles are a common part of programming on the MS Windows platform. We use handles for processes, threads, windows, GDI resources, etc. Handles are important because they provide a way for application developers to manipulate objects owned by the operating system or by other processes. If you have ever written a custom control, started a process or thread, or created an event object, you have worked with handles.

When working with multi-threaded and even multi-process applications, it can be useful to use this same mechanism to control access to objects in memory. Using handles instead of actual memory pointers can improve the durability of a system by preventing access to non-owned memory and helping to insure that no memory is leaked from the system.

The Problem

In 2001, I needed to develop several NT Services for my company's product line. When I was designing these services, I identified numerous goals which they needed to meet. My goals were ambitious but attainable. The first three goals I list are typically a requirement for all NT Service type applications.

  1. High performance
  2. Highly durable
  3. Easy to maintain
  4. etc. (Many more product specific goals.)

These services can launch hundreds of threads and these threads need to share numerous resources many of which are not determined until runtime. As with all multi-threaded apps, I was concerned with avoiding dead-lock conditions or inefficiency problems. Also, I needed to keep the code simple so that it could be easily maintained and updated.

The Solution

After some research, I decided to use a HANDLE-object mapping system to map my run-time created objects to logical handles. This would allow me to abstract much of the housekeeping involved with synchronization and thereby simplify the code. By using handles, I could more easily obtain my second and third goals. Using handles, however, does add a layer of code which must be efficient so it does not interfere with the high performance goal.

In its simplest form, handle management consists of a map of handle values to memory locations. However, as with many problems, the simplest solution is often too simple. This simple case doesn't really make the code more maintainable and offers only limited durability. Therefore, I needed to develop a solution which would implement support for all the features needed to attain my goals.

Being a C++ developer, the obvious solution is to wrap handle management into a self contained class. By wrapping handle management into a class, I can reuse the class across all my projects as needed. Also, it is easier to hide the complexity of the solution from the rest of the application. My first step was to determine what the requirements of a handle management class would be.

REQUIREMENTS

  • Must be thread safe. (Single process is OK)
  • Must be fast.
  • Must allow lookup by HANDLE and reverse lookup of HANDLE by object.
  • Must consume as little memory as possible.
  • Must support mapping all the various object types I need.
  • Must enforce type compatibility rules for objects mapped to handles.
  • Must be safe to run for long periods of time.
  • Must produce unique handle values (an upper limit is acceptable but must be rare).
  • Must support re-mapping of objects to handles.
  • Must be easily customized to each application.
  • Must support locking of handles and their associated objects.

WISH LIST

  • Provide diagnostic information on handle usage, object types, locks etc.
  • Be able to work in MFC and non MFC environment.

Basic Usage

This class exposes a complete interface for generating, maintaining and using handles. Because this class is so important to my applications, I took special care to insure that the code was well commented. You should be able to read the code fairly easily.

This class can be used "out-of-the-box" with no changes at all. In an ideal situation, you will probably want to customize it though a set of #define statements provided for this purpose. Customizing the class is ideal because you can better control type-safety and a number of other options. I discuss these #define statements later in this article.

There are nine functions exposed by this class as public methods. These are:

GetHandle

HMHANDLE GetHandle(HMOBJECTTYPE pObj, short sType)

Retrieves a handle value for the specified object. If a handle has not previously been assigned to the specified object, a handle will be generated and the new handle will be returned.

GetObject

GetObject(HMHANDLE hHandle, HMOBJECTTYPEREF pObj, short sType)

Retrieves an object based on the handle specified. This function will insure that the object mapped internally is of the type you are requesting by sType.

RemoveObject

short RemoveObject(HMHANDLE hHandle, short sType)

Removes an object from the handle manager based on the hHandle specified. This function verifies that the object mapped to hHanlde is of type sType. Objects cannot be removed if they are currently locked by a non-calling thread or another thread is waiting to obtain a lock on the object.

RemapHandle

short RemapHandle(HMHANDLE hHandle, HMOBJECTTYPE pObj, short sType)

Remaps a handle so that it is mapped to the object specified by pObj and is of type sType. Objects cannot be remapped if they are currently locked by a non-calling thread or another thread is waiting to obtain a lock on the object.

LockHandle

bool LockHandle(HMHANDLE hHandle)

Obtains a lock on the handle specified and its mapped object. If the handle is already locked by another thread, this function will block until the other thread releases its lock. This function does not guarantee that another thread won't change the underlying object unless the code using this class follows the LockHandle/UnlockHandle methodology. A single thread can call this function multiple times on a single handle safely.

UnlockHandle

bool UnlockHandle(HMHANDLE hHandle)

Releases a lock previously obtained by calling LockHandle. This function must be called once for each call to LockHandle. Failing to call UnlockHandle will result in handles remaining locked and will cause a dead-lock condition if another thread attempts to lock the handle.

IsHandleLocked

bool IsHandleLocked(HMHANDLE hHandle)

Determines whether or not a specified handle is currently locked by another thread. If the thread holding the lock calls this function, this function will return false. The result of this function is not guaranteed to be valid as it is possible for a thread to acquire a lock between this function ending and the calling function acting upon the return value.

LockManager

bool LockManager()

Blocks access to the handle manager for all threads except the calling thread until the UnlockManager function is called. Be sure to call UnlockManager for each call to LockManager. This function is implemented to support some specific requirements I have in my system. Warning: Using this function can be dangerous especially if you subsequently call LockHandle which might block your thread but would not release its hold on the manager. This would create a deadlock because the thread locking the desired handle would never be able to release it.

UnlockManagerUnblocks access to the handle manager. Should be called one time for each call to LockManager.

In addition to these nine exposed public methods, this class exposes five additional methods which provide support for maintaining usage statistics and determining the number of handles currently being managed. These are:

GetTotalCurrentHandles

long GetTotalCurrentHandles()

Returns the number of handles currently being managed by the handle manager.

AttachUsageData

void AttachUsageData(CHandleUsage* pData)

Attaches a CHandleUsage object to the CHandleManager class, and if the HM_INCLUDE_DIAGNOSTICS diagnostics option is enabled, will cause the CHandleManager class to begin maintaining detailed statistics on the handles it allocates, releases and locks.

DetachUsageData

CHandleUsage* DetachUsageData()

Detaches the CHandleUsage object attached with the AttachUsageData function. This function should be called prior to destroying the CHandleUsage object and prior to calling any of the CHandleUsage member functions directly.

CollectCurrentUsageData

void CollectCurrentUsageData(CHandleUsage* pData)

Collects statistics on the currently managed handles and updates the CHandleUsage class with that information.

How it Works

By this time, you are probably wondering how this class does what it does. This class defines a number of support classes. The most important of these is the CHandleEntry class. Internally, the CHandleManager class maintains two maps: a handle map and an object map. Whenever a handle is assigned to an object, an entry is made in each of these maps. The handle map is a map of HMHANDLE to CHandleEntry values. The object map is a map of HMOBJECTTYPE to CHandleEntry values.

One of the members of the CHandleEntry class is a short named m_sType. This is a value which is application defined and is used to match object types at runtime. When you call GetHandle, RemoveObject, or GetObject, you pass in this value and it is maintained throughout the life of the handle. This value is cross-checked on each call subsequent to the first GetHandle call where the CHandleEntry object is created and a new HMHANDLE value is assigned to the object. GetHandle, GetObject and RemoveObject will only succeed if the handle is mapped to the type specified in the call.

Whenever a call is made to GetHandle and the object is not already associated with a handle, the class creates a CHandleEntry class and populates it with the data supplied on the object along with a newly generated handle value. Then the GetHandle function inserts an entry into the handle map and the object map objects. Subsequent calls to GetHandle for a given object can be quickly resolved by performing a lookup on the object map. When the GetObject function is called to resolve an object from a handle, the GetObject function uses the handle map to lookup the associated CHandleEntry, and from there it retrieves the object. This implementation requires more memory overall but provides better performance on lookups and object resolution requests.

Locking support provides a uniform method for locking objects maintained by the handle manager. Locking support does not provide absolute thread safety because in order for it to work, all threads accessing objects maintained by the handle manager must first lock the handle prior to accessing/modifying it. For my needs, this was an acceptable situation because all my code was designed to work with this class specifically.

CHandleManager defines another class (CHandleEntryLockData) to assist with locking support. Each CHandleEntry object can contain a pointer to an allocated CHandleEntryLockData object. This class maintains several variables needed to insure that one and only one thread can lock a handle at a time. The most important member of this class is a CRITICAL_SECTION object which is used to serialize access to the handle. The CHandleEntryLockData object is only allocated when a lock is requested, and is deleted when the lock is released and no other threads are waiting on the handle. This reduces overall memory usage drastically.

How Handle Values are Generated

The base implementation of the CHandleManager class defines a HMHANDLE as a long. The valid range for HMHANDLE values is defined by the HM_MIN_HANDLE_VALUE and HM_MAX_HANDLE_VALUE. By default, these are defined as 1000 and LONG_MAX-1. The CHandleManager class maintains a HMHANDLE value (m_lNextHandle) internally which represents the last generated handle value. This value is initialized to HM_MIN_HANDLE_VALUE at class construction.

Each time GetNextHandleValue is called, the class increments m_lNextHandle until it reaches HM_MAX_HANDLE_VALUE. When this happens, the class does one of two things. It either looks for the first unused handle value by starting from HN_MIN_HANDLE_VALUE and incrementing it until it finds an unused handle, or if the m_FreeHandleLog set contains free handle values, it uses a value from this set and then removes it before continuing. It is possible that the m_FreeHandleLog set will be empty which is why the manual search mechanism is provided. Theoretically, this situation will never occur because it is unlikely that anyone will maintain two billion handles concurrently. The first situation, however, is possible as it is possible that two billion handles will be created and released over time.

When a handle is released, the released handle value is added to the m_FreeHandleLog set until the m_FreeHandleLog reaches a size of HM_MAX_FREE_HANDLE_LOG_SIZE which defaults to 1000. By maintaining this set, we can quickly obtain unused handle values once the m_lNextHandle value has reached HM_MAX_HANDLE_VALUE.

Performance Data

In addition to basic handle and locking support, the CHandleManager class supports a standard mechanism for obtaining detailed usage statistics on the manager. These statistics include the # of handles generated, # released, # locked, as well as detailed information by the various object types. This information can be very useful in diagnosing the overall performance of an app as well as in identifying potential problems. It is also helpful information to have at runtime to monitor an application.

The CHandleManager class defines another class (CHandleUsage) which can be created externally from the CHandleManager and then attached to a CHandleManager object. While it is attached to the CHandleManager class, it cannot be accessed externally but it maintains statistics on the various elements it supports. At any time, you can Detach this object from the handle manager and optionally obtain current handle statistics by calling CollectCurrentUsageData.

For more detailed information on the CHandleUsage class, see its implementation.

Using this Class in Your Application

First, you need to add the CHandleManager.h and CHandleManager.cpp files to your project.

Next, you need to decide how you want to use the class. One simple way to use the class is to instantiate a static instance of the class somewhere globally in your app. See example:

some.h file

static CHandleManager m_HandleManager;
some.cpp file
CHandleManager CSomeClass::m_HandleManager;

With this method, you can use one handle manager object throughout your app. Also, you can instantiate numerous copies of the class for various parts of your app. This is up to you and is determined by your design goals.

Once you have instantiated a copy of this class, you can begin using it immediately. See the examples below:

// to get a handle associated with an object
HMHANDLE hHandle = m_HandleManager.GetHandle(pSomeObject, MYTYPE_CMyClass);
ASSERT(HM_VALID(hHandle));

// to get the object associated with a handle
// first, lock a handle.
if (m_HandleManager.LockHandle(hHandle))
{
   CMyClass* pObject = NULL;
   short sReturn = m_HandleManager.GetObject(hHandle, 
                        (HMOBJECTTYPEREF)pObject, MYTYPE_CMyClass);

   if (sReturn == HM_NO_ERROR)
   {
   }

   m_HandleManager.UnlockHandle(hHandle);
}

sType?

You may be wondering what the significance of the sType value passed into the GetHandle, GetObject, RemoveHandle, RemapHandle functions is. This value is an application defined value which can be any valid short value. This value can always be set to zero if you want, but ideally you will define a specific short value for each type of object you want this handle manager to manage for you. By defining your own types, the class can insure type safety for you. For example, if you have three class objects you intend to have managed by this class, you will define three corresponding values for them to be mapped into the handle manager. See table below:

CMyClass1#define HM_CMyClass1     1
CMyClass2#define HM_CMyClass2     2
CMyClass3#define HM_CMyClass3     3
...

Weak Locking

You may have noticed that the locking support provided by this class is pretty weak. Actually, the locking support is as strong as the code which uses this class. If your code uses the LockHandle/UnlockHandle calls appropriately, you can safely assume that the object managed by a handle is locked. The reason I chose to use weak locking support was to allow for maximum flexibility in my use of this class. Some of my classes already had their own locking support in them and this additional locking was unnecessary.

Locking and CRITICAL_SECTION Objects

I am providing a more in-depth description of how locking support is implemented to answer any questions you might have about how it works.

Locking support is implemented by associating a CHandleEntryLockData object with a CHandleEntry object. When a lock is requested on a handle, the class first checks to see if a CHandleEntryLockData object data has already been allocated for the handle. If no CHandleEntryLockData object has been allocated, a new CHandleEntryLockData object is created and associated with the CHandleEntry object. The CHandleEntryLockData class contains four member variables. These are defined here:

m_lWaitingForLockCountA long value indicating the number of threads waiting to obtain a lock on the handle.
m_dwThreadWithLockA DWORD value indicating the thread ID of the thread owning the lock on the handle. This value is obtained by calling GetCurrentThreadId.
m_lLockCountA long value indicating the number of times the thread owning the lock has called LockHandle. This allows a single thread to call LockHandle and UnlockHandle multiple times.
m_crsLockA CRITICAL_SECTION object used to actually lock the handle and block threads while it is locked by another thread.

At this point, I should mention that function calls on the CHandleManager class are serialized using a CRITICAL_SECTION. This means that only one thread can be in a function body for this class at a time.

When LockHandle is called, the handle manager class increments the m_lWaitingForLockCount member variable of the CHandleEntryLockData structure and then leaves the CHandleManager critical section. It then calls EnterCriticalSection on the CRITICAL_SECTION member of the CHandleEntryLockData object. (This will block the calling thread until it can enter the critical section and obtain a lock on the handle. Also, because I call LeaveCriticalSection on the CHandleManager critical section, other threads can continue to call the class and work with other handles and objects.) When the EnterCriticalSection call returns, the function re-enters the CHandleManager critical section and then decrements the m_lWaitingForLockCount. Then it sets the m_dwThreadWithLock member of the CHandleEntryLockData object to ::GetCurrentThreadId(). Then it increments the m_lLockCount value and returns from the function.

When UnLockHandle is called, the m_lLockCount value is decremented. If m_lLockCount is zero then UnLockHandle checks the m_lWaitingForLockCount to determine if any threads are waiting to obtain a lock on the handle. If no threads are waiting, LeaveCriticalSection is called, then the CHandleEntryLockData object is destroyed. If there are threads waiting to obtain a lock on the handle, LeaveCriticalSection is called, but the CHandleEntryLockData object is not destroyed.

This mechanism simplifies development on other portions of the code because it minimizes the amount of threading related code I have to write elsewhere. By blocking the calling thread, I do not have to implement some other blocking mechanism at the thread code level. This mechanism has expedited my development cycle quite a bit.

By now, you may be wondering why I bothered to describe all of this. A question has been posed regarding the number of CRITICAL_SECTION objects the handle manager can/will create. Basically, the handle manager will create one CHandleEntryLockData object for each handle which is locked. (The CHandleEntryLockData contains a CRITICAL_SECTION member variable.) Under normal conditions, a CRITICAL_SECTION object is just a structure like any other. All you need is enough memory to be able to use it. This means that the only limit on how many CRITICAL_SECTION objects a process can create is limited only by the amount of available memory. Things get a little more complicated, however, when there is a lot of contention for a CRITICAL_SECTION. When this happens, the system will create a semaphore to synchronize access to the CRITICAL_SECTION. It does this to insure optimum performance and prevent any thread from being starved (I think this is right.). This is where a problem can occur. A process can only create up to 65,536 handles. A semaphore object is assigned a handle. This means that under heavy contention a semaphore is created for a lock. It is possible to run out of handles. (One thing I am not sure of is if the 65K limit is concurrent or total. From what I can tell, it is concurrent.)

The good news is, though, that it is highly unlikely that you will ever reach this limit. CHandleEntryLockData objects are usually very short lived objects. They are only allocated for the length of time a lock is maintained. Also, since at any time there are only a relatively small number of locks being held, there are usually only a small number of CHandleEntryLockData objects allocated. In my applications, I have never encountered a situation where more than 1000 handles were locked concurrently. Your application will have different requirements but even if you have 1000 threads running concurrently (something I wouldn't recommend) and each of those threads holds locks on 10 handles, you will have only 10000 CRITICAL_SECTION objects. Even if there was high contention for 50% of these, you would only have 5000 handles allocated concurrently for semaphores.

Locking and Deadlocks

There are drawbacks to the locking mechanism implemented in this class. One of these is that a deadlock condition is possible if two threads are attempting to lock the same handle concurrently. Example: Thread A locks handle 1, Thread B locks handle 2, then Thread A attempts to lock handle 2 and thread B attempts to lock handle 1. If this occurs, both threads will deadlock because they will forever wait for the other thread to release its lock. This situation is no different from what would typically happen in a multi-threaded application when two threads attempt this sequence of events (excluding code to deal with this situation). Another drawback is that maintenance programmers may find it difficult to understand the synchronization mechanism as it is not a typical solution (from what I have seen).

Having said that, as with all multi-threaded apps, it is important to properly design your app to catch, at the development/architectural level, issues like these and deal with them. It may be appropriate (depending on your situation) to only allocate handles for high-level objects in an object-hierarchy, not lower level ones. For example: Class A contains three objects of Class B. Only Class A should be allocated a handle. If one of the Class B objects is needed, a lock on Class A should be obtained instead of a lock on the Class B objects. In most situations, this will eliminate the potential deadlock condition because the condition in which two threads require the same object is limited.

In the future, I may implement support for a new locking model that would still block on calls to LockHandle, but it would temporarily release the locks held by the blocked thread. This is not really a solution because although it would remove the deadlock condition, it would introduce a different class of issues and add complexity to the code. What it does provide, however, is more options in how to deal with the complexities of multi-threaded apps which is the primary goal of this class.

Locking Options

Currently, this class only supports one mode of locking. This is blocked access to handles. See the section above on Locking and CRITICAL_SECTIONs for more detail on this. In the future, it would be possible to add a non-blocking mode of locking if needed. I have no plans to do this right now, but I may in the future. If anyone else is interested in taking on this task, please feel free.

Memory Usage

You may be wondering how much memory this class utilizes. I have included fairly detailed comments in the code regarding the amount of memory needed by the class. The amount of memory needed is largely dependent on the number of handles allocated and the options enabled when the class was compiled. Below is an except from the HandleManger.h file.

// EXAMPLE:
//     NO LOCKING SUPPORT
//        Concurrent handle count=10000
//          Memory w/diagnostics =   220,000 bytes 
//          Memory w/o diagnostics = 100,000 bytes 
//
//    WITH LOCKING SUPPORT 
//        Concurrent handle count=10000
//          Handles with locks=1000 (10%)
//          Memory w/diagnostics =   300,000 bytes 
//          Memory w/o diagnostics = 180,000 bytes

As you can see from the examples, locking and diagnostic support increase the memory requirements of the class. A minimum build of the class will require just 100K for 10,000 concurrently allocated handles. Diagnostic support increases this to 220K. Locking support with no diagnostics requires 180K, and locking and diagnostic support requires 300K for that same 10,000 handles. That means as much as 200% memory overhead for locking and diagnostic support.

In my applications, I enable diagnostic and locking support. I don't have exact statistics (pretty good ones, though), but I would estimate that in the most heavily utilized service in which I use this class, I allocate up to 20,000 concurrent handles. This puts memory usage at its peak at around 600K assuming 10% of handles are locked at any given time. (In reality, the percentage of locks is probably closer to 5%.)

Memory Fragmentation

This issue has never been a problem for me, but I feel I should at least mention it here. (In one installation, one of the services utilizing this class has been running for over 60 days.) As I stated earlier in this article, this class allocates various objects at runtime. Some of these objects are allocated and then freed quite frequently. Over time, this could lead to memory fragmentation and degrade system performance. In the future, I plan to partially deal with this by pre-allocating these objects in large sets, and then instead of destroying the objects, returning them to this pre-allocated pool.

I do not fully understand the impact this will have on the system as I am not an expert on how Windows allocates and manages memory. I would love to hear from someone who understands this better, though.

Customization

One of the design goals of this class was that it could be compile-time customized to include or exclude various options to optimize feature/performance/memory usage as needed. These compile-time options are controlled by numerous #define statements contained in the CHandleManager.h file. Below is a list of the #define statements provided, what they mean and how you can customize them.

HM_INCLUDE_DIAGNOSTICS

Determines whether or not diagnostic usage data is available. To disable usage data support, remark out this #define statement in the .h file.

The standard .h file includes support for usage statistics.

HM_INCLUDE_LOCK_SUPPORT

Determines whether or not lock support is available. To disable lock support, remark out this #define statement in the .h file.

The standard .h file includes support for locking.

HM_USE_STL_CONTAINERS

Determines whether STL containers are to be used in-place of MFC containers. Basically, the class can use either an MFC CMap class or a STL std::map class for its internal map objects.

If this #define is not remarked out, the class should have no dependencies on MFC.

Note: My tests indicate that the MFC CMap class outperforms the std::map class for my common handle uses.

The standard .h file disables use of STL containers in preference of MFC containers.

HM_DEFAULT_HASH_TABLE_SIZE

If MFC CMap objects are being used (see above #define), we initialize the CMap object's hash table to a particular size. This value should be a prime number at least the estimated size of the maximum # of expected handles.

The standard .h file defines this value as 9973. My tests indicated that this value produces excellent performance results.

HM_MAX_FREE_HANDLE_LOG_SIZE

As mentioned earlier in this article, this class maintains a std::set of released handle values. This set is maintained to optimize performance on retrieving handle values when all sequential handle values have been used up. To disable release-handle-logging, set this #define to 0.

The standard .h file defines this value as 1000.

HMHANDLEThis #define statement maps directly to a long. This is the default (low-level) type of handle values. If you wish, you can easily change this value to a ULONG to increase the number of available sequential handle values. If you do change this to ULONG (or any other type), be sure to change the HM_MAX_HANDLE_VALUE #define accordingly (see below).
HM_MIN_HANDLE_VALUEThis #define sets the lowest valid value for a handle. I reserve the first 1000 values for error/status values. This means that handle values should start at 1000. You can customize this value to as little as 11, but be warned, I may add additional status/error return values in the future.
HM_MAX_HANDLE_VALUEThis #define sets the highest valid value for a handle. Typically, this should be the MAX value associated with the type used for HMHANDLE. Typically this is LONG_MAX-1.
HMOBJECTTYPEThe objects mapped to handles by the handle manager must be of a specific run-time type to work. The default .h file defines this object type as a void pointer. This means that any memory address whether it points to a C++ class, a struct, or memory allocated with new, malloc, etc., can be associated with a handle value. This may not be desirable for all implementations as it requires explicit type casting of object types to get a working value. By default, this #define is set to void*. To change it to a custom base class, set the #define to CMyClass*.
HMOBJECTTYPEREFSimilar to above, but a reference to a pointer to an object. Typically defined as void*&. To change it to a custom base class, set the #define to CMyClass*&.

Support Features

A preprocessor macro named HM_VALID is provided to simplify the validation of handle values in your applications. It is defined as #define HM_VALID(hHandle) ((hHandle >= HM_MIN_HANDLE_VALUE && hHandle <= HM_MAX_HANDLE_VALUE)). You can use this macro in your code as you would use an ASSERT macro. See example below:

HMHANDLE hHandle = SomeHandle;
if (HM_VALID(hHandle))
{
  // TODO: Do something
}

Error Codes

When calling the GetObject, RemoveObject and RemapHandle functions, you should expect short return type. This value should be one of the #define'd error return codes. (See table below.) Also, the GetHandle function returns a HMHANDLE type and can be one of the below codes as well as a valid handle value. You should check the return value for these functions to insure your call succeeded.

HM_NO_ERRORThe function call was successful.
HM_TYPE_MISMATCHThe actual type of the object mapped to the handle does not match the type you specified.
HM_OBJECT_IS_NULLThe object pointer you specified is NULL. All object pointers must be non-NULL.
HM_INVALID_TYPECurrently unused.
HM_EXCEPTIONAn exception occurred within the function call.
HM_MEMORYThe function was unable to allocate memory for the requested operation.
HM_INVALID_HANDLEThe handle specified is not a valid handle.
HM_INTERNAL_ERRORAn unexpected logic error has occurred.
HM_NOT_MAPPED_TO_HANDLEThe object specified has not been mapped to a handle.
HM_NO_MORE_HANDLESThere are no more handles available to be assigned. This will occur if you call GetHandle and all handle values are being used. This is an unlikely condition.
HM_LOCKEDThe handle you requested is currently locked by another thread. You cannot perform the requested operation until the lock is released.

Future Improvements and/or Enhancements

There are numerous enhancements I would like to implement in this class:

  1. Utilize a Read/Write locking mechanism to allow multiple threads to concurrently request a handle and object values from the system. Write locking is only needed when creating and removing handle entries. Also, some code would have to be changed such as lock increments, etc.
  2. Add lock times to the statistics data to better understand how often objects are locked and for how long. I think this would include a minimum lock time, maximum lock time, and an average or mean lock time (or both).
  3. Implement support for automatically timing out handle objects. This would allow for periodic release of unused handles and their associated objects. This would need to support destroying the associated object as well as the internal data structures. Some mechanism would need to be created to enable the manager to destroy the associated objects.
  4. Implement support for out-of-memory persistence of handles and their associated objects. This would allow for data to be removed from physical memory and stored in a database or other long-term persistence medium. This mechanism would need to allow for these objects to be available even if the containing program is stopped and restarted.
  5. Implement a better (I think?) strategy for allocating the internal objects. Since tens of thousands of these objects are created during the life of the class, I think a potential memory fragmentation issue could be avoided. This may also impact performance. I will need to test the implementation to ensure that it works correctly and does not degrade performance. It should actually improve performance if implemented correctly.
  6. Implement an alternative locking strategy. (See the sections on locking support above, for more details).

Special Notes

I stripped this class from a running program. I had to make some minor changes to it to remove dependencies on my company's products. In the process of doing this, I may have introduced a bug unintentionally. I apologize in advance.

Updates

May 5, 2002

Thanks to soptest for identifying an issue with the LockHandle and UnlockHandle functions not returning true correctly. When I stripped the code from a working app, I removed several lines of code including some from these functions. One was bReturn = true;. This problem has been corrected.

soptest also mentioned that the # of CRITICAL_SECTION objects the class creates was too many. I have added a section to the article (see above) on this topic to better explain the way the class handles these objects and why it is implemented this way.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Generaldangling handles Pin
Mike Junkin18-Sep-02 8:26
Mike Junkin18-Sep-02 8:26 
GeneralRe: dangling handles Pin
Matt Gullett18-Sep-02 13:45
Matt Gullett18-Sep-02 13:45 
GeneralRe: dangling handles Pin
Mike Junkin18-Sep-02 18:51
Mike Junkin18-Sep-02 18:51 
GeneralToo many critical sections Pin
soptest1-May-02 11:42
soptest1-May-02 11:42 
GeneralRe: Too many critical sections Pin
Matt Gullett1-May-02 14:49
Matt Gullett1-May-02 14:49 
Thanks for the feedback.

soptest wrote:
1. I would replace critical section of locked handle by LONG and use InterlockedIncrement()/InterlockedDecrement(), and when check value of that variable use critical section of this manager.

I see what you are saying, but I may not be understanding you correctly. Removing the critical section and using InterlockedXXX would reduce the memory requirements of the class but would force the LockHandle function to be non-blocking. This would mean that a thread could call it and it fail to obtain a lock. Then the calling thread would be responsible for waiting until it could obtain a lock. This would also be problematic because 2 threads can call InterlockedIncrement at the same time. All it guarantees is that the value will be updated in a safe way, not that one will wait until InterlockedDecrement is called. One of the main reasons I implemented this class this way was to remove some of the thread synchronization burden from my various threads. This simplifies my development and expedites it.

soptest wrote:
2. Do not recomend to use this pattern for try-catch block. Potential deadlock

You are correct. This try/catch assembly is primarily intended to catch an exception thrown by the new call. If new were to fail a deadlock would not occur, though since the critical section has not been exited yet. But if an exception occurs lower down in the code, a deadlock is possible. I will review this situation and modify the code to prevent this situation. I have never encountered this situation in my apps, but it certainly is a possibility, although it would have to be a rare condition.

soptest wrote:
P.S. Why LockHandle() and UnLockHandle() always return "false" ?

My bad. When I pulled this class out of one of my programs I stripped several lines of code from the function which were customized specifically for my app. One of those lines was bReturn = true; I will update the code as soon as possible.

----------------------------------

The class will allocate as many CRITICAL_SECTION objects as there are locks. Once a lock is released AND no other threads are waiting for the handle, the CRITICAL_SECTION is released. I have monitored my usage of locks in my running apps and typically only hold < 1000 locks concurrently. Your situation my require much more. I am not aware of any limit on the # of critical sections an app can have except for available memory.

If you would rather have a non-blocking implementation of the LockHandle function I would recommend creating another #define and rewriting the LockHandle function. I do not have time to add this option right now, but I may be able to do it in the future.

Thanks,

Matt Gullett
GeneralNice Pin
Chris Maunder1-May-02 1:38
cofounderChris Maunder1-May-02 1:38 

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

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