- 通过队列元素的增减,管理内存的扩张与缩减
- 容量可调整,作为队列平衡了生产者消费者的不平衡
- 还可防止死锁,将容量增加到生产者不需等待的大小
- 队列元素有权重,默认为1;最大权重是队列容量;权重不影响顺序,还是FIFO
- 适用场景:当小容量队列会导致思索或者性能下降时;典型队列大小比一般队列小得多;需要支持元素不同权重。
- 不适用场景:不支持动态内存分配时;最大容量较小,可用定长的MPMCQueue;若没有增长过大的风险,可以用UnboundedQueue
1、基本定义
template <
typename T,
bool SingleProducer, // 生产者
bool SingleConsumer, // 消费者
bool MayBlock,
size_t LgSegmentSize = 8,
size_t LgAlign = 7,
typename WeightFn = DefaultWeightFn<T>,// 权重函数
template <typename> class Atom = std::atomic>
class DynamicBoundedQueue {
using Weight = uint64_t;
enum WaitingState : uint32_t {
NOTWAITING = 0,
WAITING = 1,
};
static constexpr bool SPSC = SingleProducer && SingleConsumer;
static constexpr size_t Align = 1u << LgAlign;
static_assert(LgAlign < 16, "LgAlign must be < 16");
/// 数据成员
// Read mostly by producers
alignas(Align) Atom<Weight> debit_; // 只有生产者频繁写这个变量
Atom<Weight> capacity_; // 容量重置时写(极少)
// Read mostly by consumers
alignas(Align) Atom<Weight> credit_; // 只有消费者会频繁写这个变量
Atom<Weight> threshold_; // 容量重置时会写(极少)
// 消费者和生产者会读写(极少)
// 容量满时消费者可能会频繁读取
alignas(Align) Atom<Weight> transfer_;
detail::Futex<Atom> waiting_;
// Underlying unbounded queue
UnboundedQueue<
T,
SingleProducer,
SingleConsumer,
MayBlock,
LgSegmentSize,
LgAlign,
Atom>
q_;
2、元素入队列
FOLLY_ALWAYS_INLINE void enqueue(const T& v) {
enqueueImpl(v);
}
……
FOLLY_ALWAYS_INLINE void enqueueImpl(Arg&& v) {
tryEnqueueUntilImpl(
std::forward<Arg>(v), std::chrono::steady_clock::time_point::max());
}
……
template <typename Clock, typename Duration, typename Arg>
FOLLY_ALWAYS_INLINE bool tryEnqueueUntilImpl(
Arg&& v,
const std::chrono::time_point<Clock, Duration>& deadline) {
Weight weight = WeightFn()(std::forward<Arg>(v)); // 计算权重
if (LIKELY(tryAddDebit(weight))) { // 尝试加上新元素的权重
q_.enqueue(std::forward<Arg>(v)); // 加成功后元素入队列,调用UnboundedQueue类型成员的enqueue函数
return true;
}
return tryEnqueueUntilSlow(std::forward<Arg>(v), deadline); // 加权重失败,再试一次
}
FOLLY_ALWAYS_INLINE bool tryAddDebit(Weight weight) noexcept {
Weight capacity = getCapacity();
Weight before = fetchAddDebit(weight); // 原子的增加权重,根据是否单个消费者分别处理
if (LIKELY(before + weight <= capacity)) { // 权重总和最大不超过容量
return true;
} else {
subDebit(weight); // 否则在总权重中减去此次权重,前提是足够减
return false;
}
}
元素入队列失败(一般是权重超过总和了)再次尝试:
template <typename Clock, typename Duration, typename Arg>
bool tryEnqueueUntilSlow(
Arg&& v,
const std::chrono::time_point<Clock, Duration>& deadline) {
Weight weight = WeightFn()(std::forward<Arg>(v));
if (canEnqueue(deadline, weight)) { // 判断是否可以加入队列,可以进入等待
q_.enqueue(std::forward<Arg>(v));
return true;
} else {
return false;
}
}
……
template <typename Clock, typename Duration>
bool canEnqueue(
const std::chrono::time_point<Clock, Duration>& deadline,
Weight weight) noexcept {
Weight capacity = getCapacity();
while (true) { // 放在while 循环内一直尝试加权
tryReduceDebit(); // 尝试减权重,是尝试将总权重减去一个transfer的值??
Weight debit = getDebit();
if ((debit + weight <= capacity) && tryAddDebit(weight)) { // 加权成功直接返回
return true;
}
if (deadline < Clock::time_point::max() && Clock::now() >= deadline) {
return false; // 超时了
}
if (MayBlock) { // 可以加锁
if (canBlock(weight, capacity)) { // 剩余权重不够,先进入等待,等消费者唤醒。
detail::futexWaitUntil(&waiting_, WAITING, deadline);
}
} else {
asm_volatile_pause();
}
}
}
插入队列:
调用的是UnboundedQueue 成员的enqueue函数
3、元素出队列
FOLLY_ALWAYS_INLINE void dequeue(T& elem) {
q_.dequeue(elem); // 调用成员无界队列的dequeue函数
addCredit(WeightFn()(elem));
}
FOLLY_ALWAYS_INLINE void addCredit(Weight weight) noexcept {
Weight before = fetchAddCredit(weight);
Weight thresh = getThreshold();
if (before + weight >= thresh && before < thresh) {
transferCredit();
}
}
void transferCredit() noexcept {
Weight credit = takeCredit();
transfer_.fetch_add(credit, std::memory_order_acq_rel);
if (MayBlock) {
std::atomic_thread_fence(std::memory_order_seq_cst);
waiting_.store(NOTWAITING, std::memory_order_relaxed);
detail::futexWake(&waiting_); // 唤醒,例如阻塞在入队列的生产者
}
}
这里的流程是,消费者消费了记录释放了空间,增加credit,意思是信用,而生产者要入队列,消耗了空间,增加的是debit,意思是借款,然后又转移到transfer里,消费者如果发现credit超出限额可以从transfer中取一些。
这个动态队列因为内部是一个无界队列,不是固定的空间,所以比定长队列可以节省空间。“动态”在于可以用reset_capacity 方法重新设置容量,实现扩容或缩容。