嵌入式C++教程:对象池(Object Pool)模式
前言
内存分配是一个非常常见的事情,这是我们无法回避讨论的。任何一个生命周期需要自己掌控而非自动的对象(或者你说结构体或者说是变量都对)都需要分配堆上内存。尽管单片机上也许没有太过严格的划分,但是我们一定需要一些持久化分配的对象。
上位机中,我们往往直接采用new/delete(底层封装malloc/free)进行内存分配。但是,一般的单片机上的new/delete很容易造成内存的稀疏,而且具备不确定的延迟、以及在某些平台上不可接受的失败风险。
这些实时性的特征很难允许我们像上位机那样随意而又自由的频繁使用 new/delete 或 malloc/free
这里,对象池(Object Pool)就是一种常见且实用的模式:提前分配一组对象(或内存块),运行时从池中借出对象、用完归还,从而实现确定性的内存使用与低延迟分配/回收。
什么时候用对象池
对象池可以被看做一个若干的一堆对象聚合,由于嵌入式的场景固定,一般咱们的对象尺寸和数量可预估(或有上限)。而且对象分配频繁且需要确定性延迟(比如网络包缓冲、任务对象、驱动上下文)。系统不允许运行时内存碎片(长期运行的设备、无人值守系统)。
像一些更加复杂的,比如说对象大小和最大并发数无法预先估计,或者需要弹性伸缩,对象池可能不合适。
API 设计
cpp
// 高层语义
template<typename T, size_t N, typename SyncPolicy>
class ObjectPool;
// 使用方式(伪代码)
static ObjectPool<MyObj, 16, NoLockPolicy> pool;
auto ptr = pool.try_acquire(); // 返回 nullptr 表示耗尽
ptr->init(...);
// 使用
pool.release(ptr);
我们提供 acquire()(阻塞或断言耗尽)与 try_acquire()(非阻塞、返回 nullptr)的组合。
核心实现
我们先看看一种可能的实现------
cpp
#pragma once
#include <cstddef>
#include <cstdint>
#include <new>
#include <type_traits>
// 简单断言(可替换为项目断言)
#ifndef EP_ASSERT
#include <cassert>
#define EP_ASSERT(x) assert(x)
#endif
// ========== 同步策略接口 ==========
// 这些策略为空壳或实现平台相关的保护操作
struct NoLockPolicy {
static void lock() {}
static void unlock() {}
};
// 关中断保护(伪代码,需由平台实现)
struct InterruptLockPolicy {
static inline unsigned primask_save() { unsigned p = 0; /* read PRIMASK */ return p; }
static inline void primask_restore(unsigned p) { /* write PRIMASK */ }
unsigned state;
InterruptLockPolicy() : state(primask_save()) {}
~InterruptLockPolicy() { primask_restore(state); }
};
// 基于 mutex 的保护(RTOS)
struct MutexLockPolicy {
static void lock(); // 在平台文件中实现
static void unlock();
};
// ========== 对象池实现 ==========
template<typename T, size_t N, typename Sync = NoLockPolicy>
class ObjectPool {
public:
static_assert(N > 0, "Pool size must be > 0");
static_assert(std::is_default_constructible<T>::value || std::is_trivially_default_constructible<T>::value,
"T must be default constructible or trivially default constructible for placement new usage");
ObjectPool() {
for (size_t i = 0; i < N; ++i) {
next_idx_[i] = (i + 1 < N) ? i + 1 : kInvalidIndex;
}
free_head_ = 0;
}
// 非阻塞借出,耗尽返回 nullptr
T* try_acquire() {
Sync::lock();
if (free_head_ == kInvalidIndex) {
Sync::unlock();
return nullptr;
}
size_t idx = free_head_;
free_head_ = next_idx_[idx];
used_count_++;
Sync::unlock();
T* obj = reinterpret_cast<T*>(&storage_[idx]);
// placement-new 初始化
new (obj) T();
return obj;
}
// 归还对象(必须来自本池)
void release(T* obj) {
EP_ASSERT(obj != nullptr);
size_t idx = ptr_to_index(obj);
EP_ASSERT(idx < N);
// 调用析构
obj->~T();
Sync::lock();
next_idx_[idx] = free_head_;
free_head_ = idx;
used_count_--;
Sync::unlock();
}
// 获取当前空闲/已用数量
size_t free_count() const {
return N - used_count_;
}
size_t used_count() const { return used_count_; }
private:
static constexpr size_t kInvalidIndex = static_cast<size_t>(-1);
// 未初始化的原始存储
typename std::aligned_storage<sizeof(T), alignof(T)>::type storage_[N];
size_t next_idx_[N];
size_t free_head_ = kInvalidIndex;
size_t used_count_ = 0;
static size_t ptr_to_index(T* ptr) {
uintptr_t base = reinterpret_cast<uintptr_t>(&storage_[0]);
uintptr_t p = reinterpret_cast<uintptr_t>(ptr);
EP_ASSERT(p >= base);
size_t offset = (p - base) / sizeof(storage_[0]);
return offset;
}
};
注意:
InterruptLockPolicy中对中断的读写是平台相关的,需要替换为目标 MCU 的实现(如 ARM Cortex-M 的 PRIMASK 读写)。如果使用 FreeRTOS,请把MutexLockPolicy的lock()/unlock()实现映射到xSemaphoreTake()/xSemaphoreGive()或taskENTER_CRITICAL()。
如何使用呢?
cpp
// 假设我们有一个包缓冲对象
struct Packet {
uint8_t buf[256];
size_t len;
void init() { len = 0; }
};
// 在全局或模块静态区分配池
static ObjectPool<Packet, 8, NoLockPolicy> pktPool;
void on_receive() {
Packet* p = pktPool.try_acquire();
if (!p) {
// 资源耗尽:丢包或记录错误
return;
}
p->init();
// 填充 p->buf, p->len ...
// 使用完毕
pktPool.release(p);
}
对于中断上下文分配的情况,如果在 ISR 中分配/释放,务必使用 InterruptLockPolicy 或实现无锁算法;避免在 ISR 中执行复杂初始化,尽量只借出对象并将处理推到任务上下文。
快速回顾
对象池在嵌入式开发中是极为实用的工具:它能把运行时内存管理的不可预测性降低到可控范围,同时提供高效的分配/回收路径。实现时需要权衡线程安全、ISR 场景、对象构造成本与诊断能力。