健壮的 C++:对象池
从内存泄漏中恢复
引言
需要持续可用的系统必须能够从内存泄漏中恢复,这样它就不需要为了“例行维护”而周期性地关闭。本文将介绍对象池如何帮助满足这一要求。这里,对象池 不 指从未销毁的共享对象池。相反,它指的是其内存从固定大小的块池而不是堆中分配的对象。
背景
在 C++ 中,通过 `operator new` 的默认实现从堆中分配的对象必须显式删除才能将其内存返回给堆。当删除未发生时,就会发生内存泄漏,通常是因为指向对象的指针丢失。C++11 中智能指针的添加降低了这种风险,但它仍然存在(例如,如果 `unique_ptr` 被破坏)。
另一个风险是内存碎片。当不同大小的内存块从堆中分配和返回堆时,就会产生空隙。长此以往,这会减少可用内存量,除非堆管理器花费时间合并相邻的空闲区域并实现最佳拟合策略。
使用对象池可以使系统从内存泄漏中恢复并避免碎片加剧。正如我们将看到的,它还启用了其他一些有用的功能。
Using the Code
与 健壮的 C++:安全网 中的一样,我们将要看过的代码来自 Robust Services Core (RSC)。但这次,代码更容易被复制到现有项目中并进行修改以满足其需求。如果您是第一次阅读有关 RSC 的某个方面的文章,请花几分钟时间阅读这篇 前言。
类概述
与许多技术不同,通常可以在大型遗留系统中引入对象池,而无需进行大规模的重新设计。原因是分配和释放对象的内存被 `new` 和 `delete` 运算符封装。通过调整类层次结构,通常使用默认堆的这些运算符版本可以很容易地被使用对象池的版本替换。
ObjectPool
ObjectPool
是对象池的基类,其每个子类都是一个实现一个池的单例。这些子类除了使用适当的参数调用 `ObjectPool` 的构造函数之外,几乎不做任何事情。每个子类在创建时都会被添加到 `ObjectPoolRegistry` 中,该注册表会跟踪系统中的所有对象池。每个池中的块在系统初始化期间分配,以便系统在启动并运行后可以专注于其工作。
Pooled
Pooled
是内存来自池的对象的基类。它重写了 `delete` 运算符,以便在对象被删除时将块返回到其池中。它还定义了一些数据成员,供池用于管理每个块。
ObjectPoolAudit
ObjectPoolAudit
是一个线程,它会定期唤醒并调用一个函数,该函数查找并返回孤立的块到池中。应用程序仍然需要 `delete` 对象,因此审计是为了修复可能逐渐导致系统内存耗尽的内存泄漏。审计使用典型的标记-清除策略,但也可以称之为后台而不是前台垃圾收集器。它可以运行的频率低于常规垃圾收集器,而不会在执行工作时长时间冻结系统。
演练
本文中的代码已编辑,删除了可能分散对核心概念注意力的内容。这些内容在某些应用程序中很重要,在其他应用程序中则不那么重要。如果您查看代码的完整版本,您会遇到它们,因此在 已删除代码 中提供了被删除内容的摘要。
创建对象池
每个 `ObjectPool` 的单例子类都调用其基类构造函数来创建其池
ObjectPool::ObjectPool(ObjectPoolId pid, size_t size, size_t segs) :
blockSize_(0),
segIncr_(0),
segSize_(0),
currSegments_(0),
targSegments_(segs),
corruptQHead_(false)
{
// The block size must account for the header above each Pooled object.
//
blockSize_ = BlockHeaderSize + Memory::Align(size);
segIncr_ = blockSize_ >> BYTES_PER_WORD_LOG2;
segSize_ = segIncr_ * ObjectsPerSegment;
for(auto i = 0; i < MaxSegments; ++i) blocks_[i] = nullptr;
// Initialize the pool's free queue of blocks.
//
freeq_.Init(Pooled::LinkDiff());
// Set the pool's identifier and add it to the registry of object pools.
//
pid_.SetId(pid);
Singleton<ObjectPoolRegistry>::Instance()->BindPool(*this);
}
Memory::Align
将每个块对齐到底层平台的字大小(32 或 64 位)。与堆类似,对象池需要一些数据来管理其块。它在每个块的开头放置一个 `BlockHeader`
// The header for a Pooled (a block in the pool). Data in the header
// survives when an object is deleted.
//
struct BlockHeader
{
ObjectPoolId pid : 8; // the pool to which the block belongs
PooledObjectSeqNo seq : 8; // the block's incarnation number
};
const size_t BlockHeader::Size = Memory::Align(sizeof(BlockHeader));
// This struct describes the top of an object block for a class that
// derives from Pooled.
//
struct ObjectBlock
{
BlockHeader header; // block management information
Pooled obj; // the actual location of the object
};
constexpr size_t BlockHeaderSize = sizeof(ObjectBlock) - sizeof(Pooled);
供参考,以下是 `Pooled` 中与本文相关的成员
// A pooled object is allocated from an ObjectPool created during system
// initialization rather than from the heap.
//
class Pooled : public Object
{
friend class ObjectPool;
public:
// Virtual to allow subclassing.
//
virtual ~Pooled() = default;
// Returns the offset to link_.
//
static ptrdiff_t LinkDiff();
// Overridden to claim blocks that this object owns.
//
void ClaimBlocks() override;
// Clears the object's orphaned_ field so that the object pool audit
// will not reclaim it. May be overridden, but the base class version
// must be invoked.
//
void Claim() override;
// Overridden to return a block to its object pool.
//
static void operator delete(void* addr);
// Deleted to prohibit array allocation.
//
static void* operator new[](size_t size) = delete;
protected:
// Protected because this class is virtual.
//
Pooled();
private:
// Link for queueing the object.
//
Q1Link link_;
// True if allocated for an object; false if on free queue.
//
bool assigned_;
// Zero for a block on the free queue or that has just been claimed by
// its owner. Incremented each time through the audit; if it reaches a
// threshold, the block deemed to be orphaned and is recovered.
//
uint8_t orphaned_;
// Used by audits to avoid invoking functions on a corrupt block. The
// audit sets this flag before it invokes any function on the object.
// If the object's function traps, the flag is still set when the audit
// resumes execution, so it knows that the block is corrupt and simply
// recovers it instead of invoking its function again. If the function
// returns successfully, the audit immediately clears the flag.
//
bool corrupt_;
// Used to avoid double logging.
//
bool logged_;
};
构造函数初始化了一个队列(`freeq_`),用于尚未分配的块。RSC 提供了两个队列模板,`Q1Way` 和 `Q2Way`。它们的实现与 STL 队列不同,因为它们在系统初始化后从不分配内存。相反,将被排队的类的对象提供一个 `ptrdiff_t` 偏移量到一个数据成员,该数据成员充当队列中下一项的链接。该偏移量是 `freeq_.Init()` 的参数。
一个关键的设计决策是池的数量。推荐的方法是为所有从同一主要框架类派生的类使用一个通用池。例如,RSC 的 `NodeBase` 为线程间消息传递缓冲区的创建定义了一个对象池。请注意,池的块大小必须比例如 `sizeof(MsgBuffer)` 稍大,以便子类有空间添加自己的数据。
为所有从同一框架类派生的类使用通用池可以大大简化池大小的工程。每个池必须有足够的块来处理峰值负载时,当系统达到最大值时。如果每个子类都有自己的池,每个池都需要足够的块来处理该子类碰巧特别受欢迎的时候。让子类共享一个池可以平滑这些波动,从而减少所需的块总数。
增加对象池的大小
无论池是在系统初始化期间创建其初始块池,还是在服务期间分配更多块,代码都是相同的
bool ObjectPool::AllocBlocks()
{
auto pid = Pid();
while(currSegments_ < targSegments_)
{
// Allocate memory for the next group of blocks.
//
auto size = sizeof(uword) * segSize_;
blocks_[currSegments_] = (uword*) Memory::Alloc(size, false);
if(blocks_[currSegments_] == nullptr) return false;
++currSegments_;
totalCount_ = currSegments_ * ObjectsPerSegment;
// Initialize each block and add it to the free queue.
//
auto seg = blocks_[currSegments_ - 1];
for(size_t j = 0; j < segSize_; j += segIncr_)
{
auto b = (ObjectBlock*) &seg[j];
b->header.pid = pid;
b->header.seq = 0;
b->obj.link_.next = nullptr;
b->obj.assigned_ = false;
b->obj.orphaned_ = OrphanThreshold;
EnqBlock(&b->obj);
}
}
return true;
}
请注意,块是按每 1K 块的段分配的,因此单个块由 `blocks_[i][j]` 定址。这使得在系统运行时轻松分配更多块:只需添加另一个段。段中的块是连续的事实对其他稍后出现的理由很有用。
创建已入池对象
让我们将注意力转向创建驻留在 `ObjectPool` 块中的对象。这以通常的方式完成,但对 `new` 的调用最终会调用框架类中一个使用该池的实现
void* MsgBuffer::operator new(size_t size)
{
return Singleton<MsgBufferPool>::Instance()->DeqBlock(size);
}
也许您在 `Pooled` 中注意到了这一点,它是所有已入池对象的基类
static void* operator new[](size_t size) = delete;
这禁止分配已入池对象的数组,因为这需要找到一组相邻的空闲块并将每个块从其在空闲队列中的位置移除。跳过。
每个基类实现的 `operator new` 调用时所执行的代码如下所示
Pooled* ObjectPool::DeqBlock(size_t size)
{
auto maxsize = blockSize_ - BlockHeaderSize;
if(size > maxsize)
{
throw AllocationException(mem_, size);
}
// If the free queue is empty, invoke UpdateAlarm, which will also
// allocate another segment.
//
auto empty = false;
if(freeq_.Empty())
{
UpdateAlarm();
}
auto item = freeq_.Deq();
if(item == nullptr)
{
throw AllocationException(mem_, size);
}
--availCount_;
return item;
}
当一个对象不适合其池的块时,最简单的解决方案是增加块的大小。小幅增加是可以容忍的,但如果一个类的对象比使用该池的其他对象大得多,可能会浪费太多内存。在这种情况下,有问题的类必须使用 PIMPL idiom 将部分数据移至一个 `private` 对象。默认情况下,此对象将从堆中分配。但是,也可以为此目的提供辅助数据块。它们也使用对象池,并有各种大小,例如小型、中型和大型。它们的实现并不困难,因此 RSC 最终将包含它们。
删除已入池对象
当对已入池对象调用 `delete` 时,它最终会找到这个
void Pooled::operator delete(void* addr)
{
auto obj = (Pooled*) addr;
auto pid = ObjectPool::ObjPid(obj);
auto pool = Singleton<ObjectPoolRegistry>::Instance()->Pool(pid);
if(pool != nullptr) pool->EnqBlock(obj, true);
}
在这里,`ObjectPool::ObjPid` 从 `BlockHeader.pid` 获取对象池的标识符,该标识符出现在前面,并且位于 `obj` 的正上方。这允许运算符将块返回到正确的池
void ObjectPool::EnqBlock(Pooled* obj)
{
if(obj == nullptr) return;
// If a block is already on the free queue or another queue, putting it
// on the free queue creates a mess.
//
if(!obj->assigned_)
{
if(obj->orphaned_ == 0) return; // already on free queue
}
else if(obj->link_.next != nullptr) return; // on some other queue
// Trample over some or all of the object.
//
auto nullify = ObjectPoolRegistry::NullifyObjectData();
obj->Nullify(nullify ? blockSize_ - BlockHeader::Size : 0);
// Return the block to the pool.
//
auto block = ObjToBlock(obj);
if(block->header.seq == MaxSeqNo)
block->header.seq = 1;
else
++block->header.seq;
obj->link_.next = nullptr;
obj->assigned_ = false;
obj->orphaned_ = 0;
obj->corrupt_ = false;
obj->logged_ = false;
if(!freeq_.Enq(*obj)) return;
++availCount_;
}
请注意,我们在将对象返回到池之前就破坏了它。破坏的数量由一个配置参数决定,该参数被读入 `nullify`。这允许整个块在测试期间用类似 `0xfd` 的字节填充。在已发布的软件中,通过仅破坏对象顶部来节省时间。这类似于许多堆的“调试”版本。其思想是,通过破坏对象,可以更有可能检测到陈旧的访问(对已删除数据的访问)。即使我们只破坏对象顶部,我们也会破坏其 vptr
,如果有人稍后尝试调用其 virtual
函数之一,这将导致异常。
审计池
前面我们提到 `ObjectPoolAudit` 使用标记-清除策略来查找孤立的块并将它们返回到它们的池。它的代码很简单,因为 `ObjectPoolRegistry` 实际上拥有所有对象池。因此,线程只是定期唤醒并告诉注册表审计池
void ObjectPoolAudit::Enter()
{
while(true)
{
Pause(interval_);
Singleton<ObjectPoolRegistry>::Instance()->AuditPools();
}
}
审计有三个不同的阶段。当前 `phase_` 和正在审计的池的标识符(`pid_`)是 `ObjectPoolAudit` 本身的成员,但由此代码使用
void ObjectPoolRegistry::AuditPools() const
{
auto thread = Singleton<ObjectPoolAudit>::Instance();
// This code is stateful. When it is reentered after an exception, it
// resumes execution at the phase and pool where the exception occurred.
//
while(true)
{
switch(thread->phase_)
{
case ObjectPoolAudit::CheckingFreeq:
//
// Audit each pool's free queue.
//
while(thread->pid_ <= ObjectPool::MaxId)
{
auto pool = pools_.At(thread->pid_);
if(pool != nullptr)
{
pool->AuditFreeq();
ThisThread::Pause();
}
++thread->pid_;
}
thread->phase_ = ObjectPoolAudit::ClaimingBlocks;
thread->pid_ = NIL_ID;
// [[fallthrough]]
case ObjectPoolAudit::ClaimingBlocks:
//
// Claim in-use blocks in each pool. Each ClaimBlocks function
// finds its blocks in an application-specific way. The blocks
// must be claimed after *all* blocks, in *all* pools, have been
// marked, because some ClaimBlocks functions claim blocks from
// multiple pools.
//
while(thread->pid_ <= ObjectPool::MaxId)
{
auto pool = pools_.At(thread->pid_);
if(pool != nullptr)
{
pool->ClaimBlocks();
ThisThread::Pause();
}
++thread->pid_;
}
thread->phase_ = ObjectPoolAudit::RecoveringBlocks;
thread->pid_ = NIL_ID;
// [[fallthrough]]
case ObjectPoolAudit::RecoveringBlocks:
//
// For each object pool, recover any block that is still marked.
// Such a block is an orphan that is neither on the free queue
// nor in use by an application.
//
while(thread->pid_ <= ObjectPool::MaxId)
{
auto pool = pools_.At(thread->pid_);
if(pool != nullptr)
{
pool->RecoverBlocks();
ThisThread::Pause();
}
++thread->pid_;
}
thread->phase_ = ObjectPoolAudit::CheckingFreeq;
thread->pid_ = NIL_ID;
return;
}
}
}
审计可用块队列
在其第一阶段,`AuditPools` 调用每个池的 `AuditFreeq` 函数。该函数首先将池中的所有块标记为孤立,然后声明已在空闲队列中的块。
损坏的队列很可能导致连续的异常。因此,`AuditFreeq` 负责检测空闲队列是否损坏,如果是,则修复它
void ObjectPool::AuditFreeq()
{
size_t count = 0;
// Increment all orphan counts.
//
for(size_t i = 0; i < currSegments_; ++i)
{
auto seg = blocks_[i];
for(size_t j = 0; j < segSize_; j += segIncr_)
{
auto b = (ObjectBlock*) &seg[j];
++b->obj.orphaned_;
}
}
// Audit the free queue unless it is empty. The audit checks that
// the links are sane and that the count of free blocks is correct.
//
auto diff = Pooled::LinkDiff();
auto item = freeq_.tail_.next;
if(item != nullptr)
{
// Audit the queue header (when PREV == nullptr), then the queue.
// The queue header references the tail element, so the tail is
// the first block whose link is audited (when CURR == freeq_).
// The next block to be audited (when PREV == freeq_) is the head
// element, which follows the tail. The entire queue has been
// traversed when CURR == freeq_ (the tail) for the second time.
//
// Before a link (CURR) is followed, the item (queue header or
// block) that provided the link is marked as corrupt. If the
// link is bad, a trap should occur at curr->orphaned_ = 0.
// Thus, if we get past that point in the code, the link should
// be sane, and so its owner's "corrupt" flag is cleared before
// continuing down the queue.
//
// If a trap occurs, this code is reentered. It starts traversing
// the queue again. Eventually it reaches an item whose corrupt_
// flag *is already set*, at which point the queue gets truncated.
//
Pooled* prev = nullptr;
auto badLink = false;
while(count <= totalCount_)
{
auto curr = (Pooled*) getptr1(item, diff);
if(prev == nullptr)
{
if(corruptQHead_)
badLink = true;
else
corruptQHead_ = true;
}
else
{
if(prev->corrupt_)
badLink = true;
else
prev->corrupt_ = true;
}
// CURR has not been claimed, so it should still be marked as
// orphaned (a value in the range 1 to OrphanThreshold). If it
// isn't, PREV's link must be corrupt. PREV might be pointing
// back into the middle of the queue, or it might be a random
// but legal address.
//
badLink = badLink ||
((curr->orphaned_ == 0) || (curr->orphaned_ > OrphanThreshold));
// If a bad link was detected, truncate the queue.
//
if(badLink)
{
if(prev == nullptr)
{
corruptQHead_ = false;
freeq_.Init(Pooled::LinkDiff());
availCount_ = 0;
}
else
{
prev->corrupt_ = false;
prev->link_.next = freeq_.tail_.next; // tail now after PREV
availCount_ = count;
}
return;
}
curr->orphaned_ = 0;
++count;
if(prev == nullptr)
corruptQHead_ = false;
else
prev->corrupt_ = false;
prev = curr;
item = item->next;
if(freeq_.tail_.next == item) break; // reached tail again
}
}
availCount_ = count;
}
声明正在使用的块
在其第二阶段,`AuditPools` 调用每个池的 `ClaimBlocks` 函数。正是这个函数必须声明正在使用的块,以确保审计不会恢复它们。因此,池必须能够找到可能拥有来自该池的对象的所有对象,以便每个所有者都可以声明其对象。
声明对象的过程是系统对象模型中的级联。前面我们提到 `NodeBase` 有一个用于线程间消息传递缓冲区的池。以下代码片段说明了如何声明正在使用的缓冲区。
线程拥有用于线程间消息传递的缓冲区,因此缓冲区池通过告诉每个线程声明其对象来执行 `ClaimBlocks`。为此,它遍历 `ThreadRegistry`,该注册表跟踪系统中的所有线程。这启动了级联
void MsgBufferPool::ClaimBlocks() { Singleton<ThreadRegistry>::Instance()->ClaimBlocks(); } void ThreadRegistry::ClaimBlocks() { // Have all threads mark themselves and their objects as being in use. // for(auto t = threads_.cbegin(); t != threads_.cend(); ++t) { auto thr = t->second.thread_; if(thr != nullptr) thr->ClaimBlocks(); } }
在线程上调用 `ClaimBlocks` 很快就会达到以下代码,在该代码中,线程声明了针对它的任何消息缓冲区:
void Thread::Claim()
{
for(auto m = msgq_.First(); m != nullptr; msgq_.Next(m))
{
m->Claim();
}
}
void Pooled::Claim()
{
orphaned_ = 0; // finally!
}
恢复孤立的块
在其第三个也是最后一个阶段,`AuditPools` 调用每个池的 `RecoverBlocks` 函数,该函数会恢复孤立块。为了防止意外的竞态条件,一个块在被回收之前必须保持孤立状态超过一个审计周期。这就是常量 `OrphanThreshold` 的目的,其值为 `2`。
void ObjectPool::RecoverBlocks()
{
auto pid = Pid();
// Run through all of the blocks, recovering orphans.
//
for(size_t i = 0; i < currSegments_; ++i)
{
auto seg = blocks_[i];
for(size_t j = 0; j < segSize_; j += segIncr_)
{
auto b = (ObjectBlock*) &seg[j];
auto p = &b->obj;
if(p->orphaned_ >= OrphanThreshold)
{
// Generate a log if the block is in use (don't bother with
// free queue orphans) and it hasn't been logged yet (which
// can happen if we reenter this code after a trap).
//
++count;
if(p->assigned_ && !p->logged_)
{
auto log = Log::Create(ObjPoolLogGroup, ObjPoolBlockRecovered);
if(log != nullptr)
{
*log << Log::Tab << "pool=" << int(pid) << CRLF;
p->logged_ = true;
p->Display(*log, Log::Tab, VerboseOpt);
Log::Submit(log);
}
}
// When an in-use orphan is found, we mark it corrupt and clean
// it up. If it is so corrupt that it causes an exception during
// cleanup, this code is reentered and encounters the block again.
// It will then already be marked as corrupt, in which case it
// will simply be returned to the free queue.
//
if(p->assigned_ && !p->corrupt_)
{
p->corrupt_ = true;
p->Cleanup();
}
b->header.pid = pid;
p->link_.next = nullptr;
EnqBlock(p);
}
}
}
}
关注点
既然我们已经了解了如何实现一个可以修复损坏的空闲队列并恢复泄漏对象的对象池,那么还应该提到对象池的其他一些功能。
迭代已入池对象
`ObjectPool` 提供迭代函数来实现此目的。在下面的代码片段中,`FrameworkClass` 是其子类驻留在 `pool` 块中的类
PooledObjectId id;
for(auto obj = pool->FirstUsed(id); obj != nullptr; obj = pool->NextUsed(id))
{
auto item = static_cast<FrameworkClass*>(obj);
// ...
}
验证指向已入池对象的指针
`ObjectPool` 提供一个函数,该函数接受指向已入池对象的指针并返回对象在池中的标识符。此标识符是介于 1 和池中块数之间的整数。如果指针无效,则函数返回 `NIL_ID`
PooledObjectId ObjectPool::ObjBid(const Pooled* obj, bool inUseOnly) const
{
if(obj == nullptr) return NIL_ID;
if(inUseOnly && !obj->assigned_) return NIL_ID;
// Find BLOCK, which houses OBJ and is the address that we'll look for.
// Search through each segment of blocks. If BLOCK is within MAXDIFF
// distance of the first block in a segment, it should belong to that
// segment, as long as it actually references a block boundary.
//
auto block = (const_ptr_t) ObjToBlock(obj);
auto maxdiff = (ptrdiff_t) (blockSize_ * (ObjectsPerSegment - 1));
for(size_t i = 0; i < currSegments_; ++i)
{
auto b0 = (const_ptr_t) &blocks_[i][0];
if(block >= b0)
{
ptrdiff_t diff = block - b0;
if(diff <= maxdiff)
{
if(diff % blockSize_ == 0)
{
auto j = diff / blockSize_;
return (i << ObjectsPerSegmentLog2) + j + 1;
}
}
}
}
return NIL_ID;
}
区分块的实例
BlockHeader
在 **创建对象池** 中被介绍过。EnqBlock
增加了其 `seq` 成员,该成员充当实例编号。这在分布式系统中很有用。假设一个已入池的对象从另一个处理器接收消息,并且它向该处理器提供了其 `this` 指针。该处理器将该指针包含在要传递给该对象的每条消息中。然后可能发生以下情况
- 对象被删除,其块很快被分配给一个新对象。
- 在其他处理器得知删除之前,它向该对象发送一条消息。
- 消息到达并被传递给新对象。
对象不应提供 `this` 作为其消息地址,而应提供其 `PooledObjectId`(由上述函数返回)及其实例编号。这样,就可以轻松检测到消息是否已过时并将其丢弃。请注意,如果对象未入池,则需要其他机制来检测过时的消息。
对象通过上面的 `ObjectPool::ObjBid` 获取其 `PooledObjectId`。反向映射由 `ObjectPool::BidToObj` 提供,而 `ObjectPool::ObjSeq` 允许对象获取其实例编号。
更改对象池的大小
每个池的大小由一个配置参数设置,该参数的值在系统初始化时从文件中读取。这允许自定义每个池的大小,这在服务器中很重要。
当系统运行时,可以通过 CLI 命令 `>cfgparms` `set` `<parm>` `<size>` 来增加池的大小,从而更改其配置参数的值。
由于池的正在使用的块可能已从其任何 1K 块段中分配,因此不支持在系统运行时减小池的大小。如果配置参数设置为较低的值,则直到下次重新分配池块的重启才会生效。(重启比重启严重程度低:请参阅 健壮的 C++:初始化和重启。)
当池中可用块的数量低于阈值时,池会发出警报。警报的严重性(次要、主要或紧急)取决于可用块的数量(少于池中总块数的 1/16、1/32 或 1/64)。警报作为警告,表明池的大小可能设计不足。
尽管系统可以承受适度的分配失败次数,但不太可能承受大量的分配失败。因此,当调用 `DeqBlock` 且没有可用块时,池会自动增加其大小一个段(1K 块)。
已删除代码
软件的完整版本包含了一些用于本文目的而被删除的方面1
- 配置参数,在 **更改池大小** 中提到。
- 日志。代码在恢复孤立块时生成日志,但许多其他情况也会产生日志。
- 统计信息。每个池都提供统计信息,例如从池中分配块的次数以及池中剩余可用块的低水位标记。
- 警报,在 **更改池大小** 中提到。
- 内存类型。RSC 根据内存是否会被写保护以及它将能够从哪个级别的重启中恢复来定义内存类型。当它调用 `ObjectPool` 的构造函数时,子类会指定用于池块的内存类型。
- 跟踪工具。大多数函数在其第一个语句中调用 `Debug::ft`。它支持函数跟踪工具以及《健壮的 C++:安全网》中提到的其他内容。还有一个跟踪工具,用于记录分配、声明和释放的对象块。
注释
1 实际上,其中大部分将是面向切面的编程中的切面。
历史
- 2020年9月7日:添加有关修改池大小的部分
- 2019年9月3日:初始版本