65.9K
CodeProject 正在变化。 阅读更多。
Home

句柄管理 - 完整解决方案

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (8投票s)

2002年5月1日

CPOL

27分钟阅读

viewsIcon

88083

downloadIcon

1087

一个用于管理对象到句柄映射的实用类。

引言

句柄是MS Windows平台上编程的常见部分。我们使用句柄来处理进程、线程、窗口、GDI资源等。句柄很重要,因为它们为应用程序开发人员提供了一种操作操作系统或其他进程拥有的对象的方式。如果您曾经编写过自定义控件、启动过进程或线程,或者创建过事件对象,那么您就已经使用过句柄。

在处理多线程甚至多进程应用程序时,使用相同的机制来控制对内存中对象的访问会很有用。使用句柄而不是实际的内存指针可以通过防止访问非拥有内存并有助于确保系统没有内存泄漏来提高系统的耐用性。

问题

2001年,我需要为公司的产品线开发多个NT服务。在设计这些服务时,我确定了它们需要满足的许多目标。我的目标雄心勃勃但可以实现。我列出的前三个目标通常是所有NT服务类型应用程序的要求。

  1. 高性能
  2. 高耐用性
  3. 易于维护
  4. 等等(更多产品特定目标)。

这些服务可以启动数百个线程,这些线程需要共享许多资源,其中许多资源直到运行时才能确定。与所有多线程应用程序一样,我关注的是避免死锁条件或效率问题。此外,我需要保持代码简单,以便易于维护和更新。

解决方案

经过一些研究,我决定使用`HANDLE`-对象映射系统来将我运行时创建的对象映射到逻辑句柄。这将允许我抽象出同步所涉及的大部分内务管理,从而简化代码。通过使用句柄,我可以更容易地实现我的第二个和第三个目标。然而,使用句柄会增加一层代码,这层代码必须高效,以免干扰高性能目标。

最简单的形式下,句柄管理由句柄值到内存位置的映射组成。然而,与许多问题一样,最简单的解决方案往往过于简单。这种简单情况并不能真正使代码更易于维护,并且只提供有限的耐用性。因此,我需要开发一个解决方案,该解决方案将实现支持实现我的目标所需的所有功能。

作为一名C++开发人员,显而易见的解决方案是将句柄管理封装到一个自包含的类中。通过将句柄管理封装到一个类中,我可以在所有项目中根据需要重用该类。此外,更容易向应用程序的其余部分隐藏解决方案的复杂性。我的第一步是确定句柄管理类的需求。

要求

  • 必须是线程安全的。(单进程可以)
  • 必须快速。
  • 必须允许通过`HANDLE`查找和通过对象反向查找`HANDLE`。
  • 必须尽可能少地消耗内存。
  • 必须支持映射我需要的所有各种对象类型。
  • 必须强制执行映射到句柄的对象的类型兼容性规则。
  • 必须能安全地长时间运行。
  • 必须生成唯一的句柄值(可接受上限,但必须罕见)。
  • 必须支持对象的句柄重新映射。
  • 必须易于针对每个应用程序进行定制。
  • 必须支持句柄及其关联对象的锁定。

愿望清单

  • 提供关于句柄使用、对象类型、锁等诊断信息。
  • 能够同时在MFC和非MFC环境中使用。

基本用法

这个类公开了一个完整的接口,用于生成、维护和使用句柄。由于这个类对我的应用程序非常重要,我特别注意确保代码注释良好。您应该能够相当容易地阅读代码。

这个类可以“开箱即用”,无需任何修改。在理想情况下,您可能希望通过为此目的提供的一组`#define`语句对其进行定制。定制该类是理想的,因为您可以更好地控制类型安全和许多其他选项。我将在本文后面讨论这些`#define`语句。

这个类公开了九个作为公共方法的函数。它们是:

GetHandle

HMHANDLE GetHandle(HMOBJECTTYPE pObj, short sType)

检索指定对象的句柄值。如果之前未将句柄分配给指定对象,则将生成一个句柄并返回新句柄。

GetObject

GetObject(HMHANDLE hHandle, HMOBJECTTYPEREF pObj, short sType)

根据指定的句柄检索对象。此函数将确保内部映射的对象是您通过`sType`请求的类型。

RemoveObject

short RemoveObject(HMHANDLE hHandle, short sType)

根据指定的`hHandle`从句柄管理器中移除对象。此函数验证映射到`hHandle`的对象是否为`sType`类型。如果对象当前被非调用线程锁定或另一个线程正在等待获取该对象的锁,则无法移除对象。

RemapHandle

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

重新映射句柄,使其映射到`pObj`指定的对象并具有`sType`类型。如果对象当前被非调用线程锁定或另一个线程正在等待获取该对象的锁,则无法重新映射对象。

LockHandle

bool LockHandle(HMHANDLE hHandle)

获取指定句柄及其映射对象的锁。如果句柄已被另一个线程锁定,此函数将阻塞,直到其他线程释放其锁。此函数不保证另一个线程不会更改底层对象,除非使用此类的代码遵循`LockHandle`/`UnlockHandle`方法。单个线程可以安全地对单个句柄多次调用此函数。

UnlockHandle

bool UnlockHandle(HMHANDLE hHandle)

释放通过调用`LockHandle`先前获得的锁。此函数必须为每次`LockHandle`调用调用一次。未能调用`UnlockHandle`将导致句柄保持锁定状态,并且如果另一个线程尝试锁定句柄,将导致死锁条件。

IsHandleLocked

bool IsHandleLocked(HMHANDLE hHandle)

确定指定句柄当前是否被另一个线程锁定。如果持有锁的线程调用此函数,此函数将返回`false`。此函数的结果不保证有效,因为线程有可能在此函数结束和调用函数根据返回值执行操作之间获取锁。

LockManager

bool LockManager()

阻塞除调用线程之外的所有线程对句柄管理器的访问,直到调用`UnlockManager`函数。请务必为每次`LockManager`调用调用`UnlockManager`。此函数旨在支持我在系统中拥有的一些特定要求。警告:使用此函数可能很危险,特别是如果您随后调用`LockHandle`,这可能会阻塞您的线程,但不会释放其对管理器的持有。这将导致死锁,因为锁定所需句柄的线程将永远无法释放它。

UnlockManager 解除对句柄管理器的访问阻塞。对于每次`LockManager`调用,应调用一次。

除了这九个公开的公共方法之外,该类还公开了另外五个方法,这些方法提供对维护使用统计信息和确定当前正在管理的句柄数量的支持。它们是:

GetTotalCurrentHandles

long GetTotalCurrentHandles()

返回句柄管理器当前管理的句柄数量。

AttachUsageData

void AttachUsageData(CHandleUsage* pData)

将`CHandleUsage`对象附加到`CHandleManager`类,如果启用了`HM_INCLUDE_DIAGNOSTICS`诊断选项,将导致`CHandleManager`类开始维护其分配、释放和锁定的句柄的详细统计信息。

DetachUsageData

CHandleUsage* DetachUsageData()

分离通过`AttachUsageData`函数附加的`CHandleUsage`对象。此函数应在销毁`CHandleUsage`对象之前以及直接调用任何`CHandleUsage`成员函数之前调用。

CollectCurrentUsageData

void CollectCurrentUsageData(CHandleUsage* pData)

收集当前管理句柄的统计信息,并使用该信息更新`CHandleUsage`类。

工作原理

到目前为止,您可能想知道这个类是如何做到这些的。这个类定义了许多支持类。其中最重要的是`CHandleEntry`类。在内部,`CHandleManager`类维护两个映射:一个句柄映射和一个对象映射。每当一个句柄分配给一个对象时,就会在每个映射中创建一个条目。句柄映射是`HMHANDLE`到`CHandleEntry`值的映射。对象映射是`HMOBJECTTYPE`到`CHandleEntry`值的映射。

`CHandleEntry`类的一个成员是一个名为`m_sType`的`short`值。这是一个应用程序定义的值,用于在运行时匹配对象类型。当您调用`GetHandle`、`RemoveObject`或`GetObject`时,您会传入这个值,并且它在句柄的整个生命周期中都会被维护。在创建`CHandleEntry`对象并为对象分配新的`HMHANDLE`值后,每次对`GetHandle`的后续调用都会对这个值进行交叉检查。`GetHandle`、`GetObject`和`RemoveObject`只有在句柄映射到调用中指定的类型时才会成功。

每当调用`GetHandle`且对象尚未与句柄关联时,该类都会创建一个`CHandleEntry`类,并用对象上提供的数据以及新生成的句柄值填充它。然后,`GetHandle`函数将一个条目插入到句柄映射和对象映射对象中。后续对给定对象的`GetHandle`调用可以通过在对象映射上执行查找来快速解决。当调用`GetObject`函数从句柄解析对象时,`GetObject`函数使用句柄映射查找关联的`CHandleEntry`,并从中检索对象。此实现总体上需要更多内存,但在查找和对象解析请求方面提供了更好的性能。

锁定支持为句柄管理器维护的对象提供了一种统一的锁定方法。锁定支持不提供绝对的线程安全,因为为了使其工作,所有访问句柄管理器维护的对象的线程必须在访问/修改之前先锁定句柄。对于我的需求,这是一个可接受的情况,因为我的所有代码都专门设计用于此类别。

`CHandleManager`定义了另一个类(`CHandleEntryLockData`)来协助锁定支持。每个`CHandleEntry`对象都可以包含指向已分配的`CHandleEntryLockData`对象的指针。此类维护几个变量,以确保一次只有一个线程可以锁定一个句柄。此类最重要的成员是一个`CRITICAL_SECTION`对象,用于序列化对句柄的访问。`CHandleEntryLockData`对象仅在请求锁定并释放锁定且没有其他线程等待句柄时才分配。这大大减少了总体内存使用量。

句柄值是如何生成的

`CHandleManager`类的基本实现将`HMHANDLE`定义为`long`。`HMHANDLE`值的有效范围由`HM_MIN_HANDLE_VALUE`和`HM_MAX_HANDLE_VALUE`定义。默认情况下,它们被定义为1000和`LONG_MAX`-1。`CHandleManager`类内部维护一个`HMHANDLE`值(`m_lNextHandle`),该值表示上次生成的句柄值。此值在类构造时初始化为`HM_MIN_HANDLE_VALUE`。

每次调用`GetNextHandleValue`时,类都会递增`m_lNextHandle`,直到它达到`HM_MAX_HANDLE_VALUE`。此时,类会执行以下两项操作之一:它会从`HN_MIN_HANDLE_VALUE`开始查找第一个未使用的句柄值并递增,直到找到一个未使用的句柄,**或者**如果`m_FreeHandleLog`集合包含空闲句柄值,它会使用此集合中的一个值,然后将其移除。`m_FreeHandleLog`集合可能为空,这就是为什么提供了手动搜索机制。理论上,这种情况永远不会发生,因为不太可能有人同时维护20亿个句柄。然而,第一种情况是可能的,因为可能会随着时间的推移创建和释放20亿个句柄。

当句柄被释放时,释放的句柄值会添加到`m_FreeHandleLog`集合中,直到`m_FreeHandleLog`达到`HM_MAX_FREE_HANDLE_LOG_SIZE`的大小(默认为1000)。通过维护此集合,一旦`m_lNextHandle`值达到`HM_MAX_HANDLE_VALUE`,我们就可以快速获取未使用的句柄值。

性能数据

除了基本的句柄和锁定支持之外,`CHandleManager`类还支持一种标准机制来获取管理器的详细使用统计信息。这些统计信息包括生成的句柄数量、释放的句柄数量、锁定的句柄数量,以及各种对象类型的详细信息。这些信息对于诊断应用程序的整体性能以及识别潜在问题非常有用。它也是在运行时监视应用程序的有用信息。

`CHandleManager`类定义了另一个类(`CHandleUsage`),该类可以在`CHandleManager`外部创建,然后附加到`CHandleManager`对象。当它附加到`CHandleManager`类时,不能从外部访问,但它维护其支持的各种元素的统计信息。您可以随时将此对象从句柄管理器中`Detach`,并可以选择通过调用`CollectCurrentUsageData`获取当前句柄统计信息。

有关`CHandleUsage`类的更详细信息,请参阅其实现。

在您的应用程序中使用此类别

首先,您需要将_CHandleManager.h_和_CHandleManager.cpp_文件添加到您的项目中。

接下来,您需要决定如何使用该类。一种简单的使用该类的方法是在您的应用程序中全局实例化该类的一个静态实例。请参见示例:

some.h file

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

使用此方法,您可以在整个应用程序中使用一个句柄管理器对象。此外,您可以为应用程序的各个部分实例化该类的多个副本。这取决于您,并由您的设计目标决定。

实例化该类的副本后,您可以立即开始使用它。请参见下面的示例:

// 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?

您可能想知道传入`GetHandle`、`GetObject`、`RemoveHandle`、`RemapHandle`函数的`sType`值的意义是什么。此值是一个应用程序定义的值,可以是任何有效的`short`值。如果您愿意,此值始终可以设置为零,但理想情况下,您将为希望此句柄管理器为您管理的每种对象类型定义一个特定的`short`值。通过定义您自己的类型,该类可以为您确保类型安全。例如,如果您有三个打算由该类管理的对象,您将为它们定义三个相应的值,以映射到句柄管理器中。请参见下表:

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

弱锁定

您可能已经注意到,该类提供的锁定支持相当弱。实际上,锁定支持与使用该类的代码一样强大。如果您的代码适当地使用`LockHandle`/`UnlockHandle`调用,您可以安全地假设由句柄管理的对象已被锁定。我选择使用弱锁定支持的原因是为了最大限度地提高该类使用的灵活性。我的一些类已经有自己的锁定支持,这种额外的锁定是不必要的。

锁定和CRITICAL_SECTION对象

我将更深入地描述锁定支持是如何实现的,以回答您可能对此工作原理的任何疑问。

锁定支持是通过将`CHandleEntryLockData`对象与`CHandleEntry`对象关联来实现的。当请求对句柄进行锁定S时,该类首先检查是否已经为该句柄分配了`CHandleEntryLockData`对象数据。如果尚未分配`CHandleEntryLockData`对象,则会创建一个新的`CHandleEntryLockData`对象并将其与`CHandleEntry`对象关联。`CHandleEntryLockData`类包含四个成员变量。它们定义如下:

m_lWaitingForLockCount 一个`long`值,表示等待获取句柄锁定的线程数量。
m_dwThreadWithLock 一个`DWORD`值,表示拥有句柄锁定的线程ID。此值通过调用`GetCurrentThreadId`获取。
m_lLockCount 一个`long`值,表示拥有锁定的线程调用`LockHandle`的次数。这允许单个线程多次调用`LockHandle`和`UnlockHandle`。
m_crsLock 一个`CRITICAL_SECTION`对象,用于实际锁定句柄并在被另一个线程锁定时阻塞线程。

此时,我应该提到,对`CHandleManager`类的函数调用是使用`CRITICAL_SECTION`进行序列化的。这意味着一次只有一个线程可以处于该类的函数体中。

当调用`LockHandle`时,句柄管理器类会递增`CHandleEntryLockData`结构体的`m_lWaitingForLockCount`成员变量,然后离开`CHandleManager`关键区。然后它调用`CHandleEntryLockData`对象的`CRITICAL_SECTION`成员上的`EnterCriticalSection`。(这将阻塞调用线程,直到它可以进入关键区并获取句柄上的锁。此外,由于我对`CHandleManager`关键区调用`LeaveCriticalSection`,其他线程可以继续调用该类并处理其他句柄和对象。)当`EnterCriticalSection`调用返回时,函数重新进入`CHandleManager`关键区,然后递减`m_lWaitingForLockCount`。然后它将`CHandleEntryLockData`对象的`m_dwThreadWithLock`成员设置为`::GetCurrentThreadId()`。然后它递增`m_lLockCount`值并从函数返回。

当调用`UnLockHandle`时,`m_lLockCount`值递减。如果`m_lLockCount`为零,则`UnLockHandle`会检查`m_lWaitingForLockCount`以确定是否有任何线程正在等待获取句柄上的锁。如果没有线程正在等待,则调用`LeaveCriticalSection`,然后销毁`CHandleEntryLockData`对象。如果有线程正在等待获取句柄上的锁,则调用`LeaveCriticalSection`,但`CHandleEntryLockData`对象不会被销毁。

这种机制简化了代码其他部分的开发,因为它最大限度地减少了我必须在其他地方编写的线程相关代码量。通过阻塞调用线程,我无需在线程代码级别实现其他阻塞机制。这种机制大大加快了我的开发周期。

现在,您可能想知道我为什么要费心描述所有这些。有人提出了关于句柄管理器可以/将创建多少个`CRITICAL_SECTION`对象的问题。基本上,句柄管理器将为每个锁定的句柄创建一个`CHandleEntryLockData`对象。(`CHandleEntryLockData`包含一个`CRITICAL_SECTION`成员变量。)在正常情况下,`CRITICAL_SECTION`对象只是一个结构,与其他任何结构一样。您所需要的只是足够的内存来使用它。这意味着进程可以创建的`CRITICAL_SECTION`对象的数量仅受可用内存量的限制。然而,当`CRITICAL_SECTION`存在大量竞争时,事情会变得有点复杂。当这种情况发生时,系统将创建一个信号量来同步对`CRITICAL_SECTION`的访问。它这样做是为了确保最佳性能并防止任何线程被饿死(我认为这是正确的。)。这就是可能出现问题的地方。一个进程最多只能创建65,536个句柄。信号量对象被分配一个句柄。这意味着在激烈的竞争下,会为锁创建一个信号量。句柄可能会耗尽。(我不确定65K的限制是并发的还是总的。据我所知,它是并发的。)

好消息是,您几乎不可能达到这个极限。`CHandleEntryLockData`对象通常是生命周期很短的对象。它们仅在保持锁定的时间内分配。此外,由于在任何时候只有相对较少数量的锁被持有,因此通常只有少量`CHandleEntryLockData`对象被分配。在我的应用程序中,我从未遇到过超过1000个句柄同时被锁定的情况。您的应用程序会有不同的要求,但即使您同时运行1000个线程(我不建议这样做),并且每个线程都持有10个句柄的锁,您也只会有10000个`CRITICAL_SECTION`对象。即使其中50%存在高竞争,您也只会为信号量并发分配5000个句柄。

锁定和死锁

该类实现的锁定机制存在一些缺点。其中之一是,如果两个线程同时尝试锁定同一个句柄,则可能出现死锁情况。例如:线程A锁定句柄1,线程B锁定句柄2,然后线程A尝试锁定句柄2,线程B尝试锁定句柄1。如果发生这种情况,两个线程都将死锁,因为它们将永远等待另一个线程释放其锁。这种情况与多线程应用程序中两个线程尝试此事件序列时通常会发生的情况没有区别(不包括处理此情况的代码)。另一个缺点是,维护程序员可能会发现难以理解同步机制,因为它不是一个典型的解决方案(据我所知)。

话虽如此,与所有多线程应用程序一样,在开发/架构层面适当设计您的应用程序以捕获和处理此类问题非常重要。根据您的情况,可能只为对象层次结构中的高级对象(而非低级对象)分配句柄。例如:类`A`包含三个类`B`的对象。只有类`A`应该分配一个句柄。如果需要其中一个类`B`对象,则应该获取类`A`上的锁,而不是类`B`对象上的锁。在大多数情况下,这将消除潜在的死锁条件,因为两个线程需要相同对象的情况是有限的。

将来,我可能会实现对新锁定模型的支持,该模型仍会阻塞对`LockHandle`的调用,但会暂时释放被阻塞线程持有的锁。这并不是真正的解决方案,因为尽管它会消除死锁条件,但它会引入不同类型的问题并增加代码的复杂性。然而,它提供了处理多线程应用程序复杂性的更多选项,这是此类别的首要目标。

锁定选项

目前,此类只支持一种锁定模式。即对句柄的阻塞访问。有关此内容的更多详细信息,请参见上面关于锁定和`CRITICAL_SECTION`的部分。将来,如果需要,可以添加一种非阻塞模式的锁定。我目前没有计划这样做,但将来可能会。如果其他人有兴趣承担这项任务,请随时联系。

内存使用

您可能想知道这个类使用了多少内存。我在代码中包含了相当详细的注释,说明了该类所需的内存量。所需的内存量很大程度上取决于分配的句柄数量以及编译类时启用的选项。下面是来自`HandleManager.h`文件的一个摘录。

// 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

从示例中可以看出,锁定和诊断支持增加了类的内存需求。类的最小构建需要100K来同时分配10,000个句柄。诊断支持将其增加到220K。没有诊断的锁定支持需要180K,而锁定和诊断支持对于相同的10,000个句柄需要300K。这意味着锁定和诊断支持的内存开销高达200%。

在我的应用程序中,我启用了诊断和锁定支持。我没有确切的统计数据(但相当好),但我估计在我使用此类的最繁忙的服务中,我分配了多达20,000个并发句柄。这使得内存使用量在其峰值时约为600K,假设在任何给定时间有10%的句柄被锁定。(实际上,锁定的百分比可能更接近5%。)

内存碎片

这个问题对我来说从来都不是问题,但我觉得至少应该在这里提一下。(在一个安装中,一个使用此类的服务已经运行了60多天。)正如我在本文前面所说,这个类在运行时分配了各种对象。其中一些对象被频繁地分配和释放。随着时间的推移,这可能导致内存碎片并降低系统性能。将来,我计划通过预先分配这些对象到大集合中来部分解决这个问题,然后不是销毁对象,而是将它们返回到这个预分配的池中。

我并不完全了解这将对系统产生的影响,因为我不是Windows如何分配和管理内存的专家。不过,我很乐意听取更了解此问题的人的意见。

自定义

此类的设计目标之一是可以在编译时定制,以根据需要包含或排除各种选项,以优化功能/性能/内存使用。这些编译时选项由_CHandleManager.h_文件中包含的众多`#define`语句控制。下面列出了提供的`#define`语句、它们的含义以及如何定制它们。

HM_INCLUDE_DIAGNOSTICS

确定诊断使用数据是否可用。要禁用使用数据支持,请在_.h_文件中注释掉此`#define`语句。

标准_.h_文件包含对使用统计数据的支持。

HM_INCLUDE_LOCK_SUPPORT

确定是否提供锁定支持。要禁用锁定支持,请在_.h_文件中注释掉此`#define`语句。

标准_.h_文件包含对锁定的支持。

HM_USE_STL_CONTAINERS

确定是否使用STL容器代替MFC容器。基本上,该类可以使用MFC `CMap`类或STL `std::map`类作为其内部映射对象。

如果此`#define`未被注释掉,则该类不应依赖于MFC。

注意:我的测试表明,对于我常用的句柄使用,MFC `CMap`类的性能优于`std::map`类。

标准_.h_文件禁用STL容器的使用,优先选择MFC容器。

HM_DEFAULT_HASH_TABLE_SIZE

如果正在使用MFC `CMap`对象(参见上面的`#define`),我们会将`CMap`对象的哈希表初始化为特定大小。此值应该是一个至少等于预期最大句柄数量的素数。

标准_.h_文件将此值定义为9973。我的测试表明此值可产生出色的性能结果。

HM_MAX_FREE_HANDLE_LOG_SIZE

正如本文前面提到的,该类维护一个已释放句柄值的`std::set`。维护此集合是为了在所有顺序句柄值都已用尽时优化检索句柄值的性能。要禁用释放句柄日志记录,请将此`#define`设置为0。

标准_.h_文件将此值定义为1000。

HMHANDLE 此`#define`语句直接映射到`long`。这是句柄值的默认(低级)类型。如果您愿意,可以轻松地将其更改为`ULONG`以增加可用顺序句柄值的数量。如果您确实将其更改为`ULONG`(或任何其他类型),请务必相应地更改`HM_MAX_HANDLE_VALUE` `#define`(参见下文)。
HM_MIN_HANDLE_VALUE 此`#define`设置句柄的最低有效值。我保留前1000个值用于错误/状态值。这意味着句柄值应从1000开始。您可以将此值定制为小至11,但请注意,我将来可能会添加额外的状态/错误返回值。
HM_MAX_HANDLE_VALUE 此`#define`设置句柄的最高有效值。通常,这应该是与`HMHANDLE`所用类型相关联的最大值。通常这是`LONG_MAX-1`。
HMOBJECTTYPE 由句柄管理器映射到句柄的对象必须是特定的运行时类型才能工作。默认的_.h_文件将此对象类型定义为`void`指针。这意味着任何内存地址,无论是指向C++类、`struct`,还是使用`new`、`malloc`等分配的内存,都可以与句柄值关联。对于所有实现来说,这可能不是理想的,因为它需要显式类型转换对象类型以获取工作值。默认情况下,此`#define`设置为`void*`。要将其更改为自定义基类,请将`#define`设置为`CMyClass*`。
HMOBJECTTYPEREF 与上述类似,但它是一个指向对象的指针的引用。通常定义为`void*&`。要将其更改为自定义基类,请将`#define`设置为`CMyClass*&`。

支持功能

提供了一个名为`HM_VALID`的预处理器宏,以简化应用程序中句柄值的验证。它定义为`#define HM_VALID(hHandle) ((hHandle >= HM_MIN_HANDLE_VALUE && hHandle <= HM_MAX_HANDLE_VALUE))`。您可以在代码中使用此宏,就像使用`ASSERT`宏一样。请参见下面的示例:

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

错误代码

调用`GetObject`、`RemoveObject`和`RemapHandle`函数时,您应该期望`short`返回值类型。此值应为`#define`定义的错误返回代码之一。(参见下表。)此外,`GetHandle`函数返回`HMHANDLE`类型,并且可以是以下代码之一以及有效的句柄值。您应该检查这些函数的返回值以确保您的调用成功。

HM_NO_ERROR 函数调用成功。
HM_TYPE_MISMATCH 映射到句柄的对象的实际类型与您指定的类型不匹配。
HM_OBJECT_IS_NULL 您指定的对象指针为`NULL`。所有对象指针都必须为非`NULL`。
HM_INVALID_TYPE 当前未使用。
HM_EXCEPTION 函数调用中发生异常。
HM_MEMORY 函数无法为请求的操作分配内存。
HM_INVALID_HANDLE 指定的句柄不是有效句柄。
HM_INTERNAL_ERROR 发生意外的逻辑错误。
HM_NOT_MAPPED_TO_HANDLE 指定对象尚未映射到句柄。
HM_NO_MORE_HANDLES 没有更多句柄可供分配。如果您调用`GetHandle`并且所有句柄值都被使用,就会发生这种情况。这是一个不太可能发生的条件。
HM_LOCKED 您请求的句柄当前已被另一个线程锁定。在释放锁定之前,您无法执行请求的操作。

未来的改进和/或增强

我希望在此类中实现许多增强功能:

  1. 利用读/写锁定机制,允许多个线程同时从系统请求句柄和对象值。只有在创建和删除句柄条目时才需要写锁定。此外,一些代码,例如锁递增等,也需要更改。
  2. 将锁定时间添加到统计数据中,以便更好地了解对象被锁定的频率和持续时间。我认为这应该包括最小锁定时间、最大锁定时间以及平均或平均锁定时间(或两者)。
  3. 实现对句柄对象自动超时的支持。这将允许定期释放未使用的句柄及其关联对象。这将需要支持销毁关联对象以及内部数据结构。需要创建某种机制来使管理器能够销毁关联对象。
  4. 实现对句柄及其关联对象的内存不足持久性支持。这将允许将数据从物理内存中移除并存储在数据库或其他长期持久性介质中。这种机制需要允许这些对象即使在包含程序停止和重新启动后也可用。
  5. 实现一个更好的(我认为?)内部对象分配策略。由于在类的生命周期内会创建数万个这些对象,我认为可以避免潜在的内存碎片问题。这还可能影响性能。我需要测试实现以确保其正常工作且不会降低性能。如果实现正确,它实际上应该能提高性能。
  6. 实施替代的锁定策略。(有关更多详细信息,请参阅上面关于锁定支持的部分)。

特别说明

我从一个正在运行的程序中剥离了这个类。我不得不对其进行一些小的更改,以消除对我公司产品的依赖。在此过程中,我可能无意中引入了一个错误。我提前表示歉意。

更新

2002年5月5日

感谢**soptest**发现`LockHandle`和`UnlockHandle`函数未能正确返回`true`的问题。当我从一个正常工作的应用程序中剥离代码时,我删除了几行代码,其中包括这些函数中的一些。其中一行是`bReturn = true;`。这个问题已经得到纠正。

**soptest** 还提到该类创建的`CRITICAL_SECTION`对象太多。我已经在文章中添加了一个关于此主题的部分(见上文),以更好地解释该类处理这些对象的方式以及为什么这样实现。

© . All rights reserved.