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.
- High performance
- Highly durable
- Easy to maintain
- 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. |
UnlockManager |
Unblocks 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:
HMHANDLE hHandle = m_HandleManager.GetHandle(pSomeObject, MYTYPE_CMyClass);
ASSERT(HM_VALID(hHandle));
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_lWaitingForLockCount |
A long value indicating the number of threads waiting to obtain a lock on the handle. |
m_dwThreadWithLock |
A DWORD value indicating the thread ID of the thread owning the lock on the handle. This value is obtained by calling GetCurrentThreadId . |
m_lLockCount |
A 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_crsLock |
A 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_SECTION
s 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.
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. |
HMHANDLE |
This #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_VALUE |
This #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_VALUE |
This #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 . |
HMOBJECTTYPE |
The 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* . |
HMOBJECTTYPEREF |
Similar 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))
{
}
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_ERROR |
The function call was successful. |
HM_TYPE_MISMATCH |
The actual type of the object mapped to the handle does not match the type you specified. |
HM_OBJECT_IS_NULL |
The object pointer you specified is NULL . All object pointers must be non-NULL . |
HM_INVALID_TYPE |
Currently unused. |
HM_EXCEPTION |
An exception occurred within the function call. |
HM_MEMORY |
The function was unable to allocate memory for the requested operation. |
HM_INVALID_HANDLE |
The handle specified is not a valid handle. |
HM_INTERNAL_ERROR |
An unexpected logic error has occurred. |
HM_NOT_MAPPED_TO_HANDLE |
The object specified has not been mapped to a handle. |
HM_NO_MORE_HANDLES |
There 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_LOCKED |
The 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:
- 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.
- 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).
- 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.
- 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.
- 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.
- 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. |