F´ Flight Software - C/C++ Documentation
A framework for building embedded system applications to NASA flight quality standards.
LocklessPriorityQueue.cpp
Go to the documentation of this file.
1 // ======================================================================
2 // \title Os/Generic/LocklessPriorityQueue.cpp
3 // \brief lockless ISR-safe priority queue implementation for Os::Queue
4 // ======================================================================
6 #include <atomic>
7 #include <cstring>
8 #include <limits>
9 #include "Fw/LanguageHelpers.hpp"
10 #include "Fw/Time/TimeInterval.hpp"
11 #include "Fw/Types/Assert.hpp"
12 #include "Fw/Types/ByteArray.hpp"
14 #include "Os/Task.hpp"
16 
17 namespace Os {
18 namespace Generic {
19 
20 namespace {
21 
23 constexpr LocklessStateTagType stateOf(LocklessStateTagType packed) {
24  return packed & LocklessSlot::STATE_MASK;
25 }
26 
28 constexpr LocklessStateTagType tagOf(LocklessStateTagType packed) {
29  return packed >> LocklessSlot::STATE_BITS;
30 }
31 
33 constexpr LocklessStateTagType packStateTag(LocklessStateTagType state, LocklessStateTagType tag) {
34  return (tag << LocklessSlot::STATE_BITS) | (state & LocklessSlot::STATE_MASK);
35 }
36 
37 } // namespace
38 
40  U32 candidateSequence,
41  FwQueuePriorityType bestPriority,
42  U32 bestSequence) {
43  bool preferred = false;
44  if (candidatePriority > bestPriority) {
45  preferred = true;
46  } else if (candidatePriority == bestPriority) {
47  // Modular subtraction: if (candidate - best) interpreted unsigned has its top bit set,
48  // candidate is "older" (smaller in the wrap-aware ordering) than best.
49  const U32 difference = candidateSequence - bestSequence;
50  const U32 topBit = static_cast<U32>(1) << (std::numeric_limits<U32>::digits - 1);
51  preferred = (difference & topBit) != 0;
52  }
53  return preferred;
54 }
55 
57  : m_stateTag(packStateTag(LOCKLESS_SLOT_FREE, 0)), m_sequence(0), m_size(0), m_priority(0) {}
58 
60  : QueueHandle(),
61  m_slots(nullptr),
62  m_slotsAllocation(nullptr),
63  m_data(nullptr),
64  m_depth(0),
65  m_messageSize(0),
66  m_sequence(0),
67  m_count(0),
68  m_available(0),
69  m_highMark(0),
70  m_id(0) {}
71 
73  // Intentionally empty: cleanup is the responsibility of an explicit `teardown()` call.
74  // Freeing here would call the `Fw::MemAllocatorRegistry` singleton, whose destruction
75  // order relative to global queue objects is unspecified (matches
76  // `Os::Generic::PriorityQueue::~PriorityQueue()`).
77 }
78 
80  const Fw::ConstStringBase& name,
81  FwSizeType depth,
82  FwSizeType messageSize) {
83  static_cast<void>(name);
84 
85  // Ensure that the queue has not already been created. A double create would leak memory and
86  // is a programming error.
87  FW_ASSERT(this->m_handle.m_slots == nullptr);
88  FW_ASSERT(this->m_handle.m_data == nullptr);
89 
90  // The state-tag word relies on the underlying atomic being lock-free for ISR safety. The
91  // static_assert in the header covers platforms with a compile-time guarantee; this runtime
92  // check is the authoritative gate (`is_always_lock_free` is C++17 and this code targets
93  // C++14).
94  std::atomic<LocklessStateTagType> probe(0);
95  FW_ASSERT(probe.is_lock_free());
96  std::atomic<FwQueuePriorityType> priorityProbe(0);
97  FW_ASSERT(priorityProbe.is_lock_free());
98  std::atomic<U32> counterProbe(0);
99  FW_ASSERT(counterProbe.is_lock_free());
100  FW_ASSERT(depth > 0);
101  FW_ASSERT(messageSize > 0);
102 
103  // Guard the multiplications used to compute allocation sizes against overflow.
104  const FwSizeType maxSize = std::numeric_limits<FwSizeType>::max();
105  FW_ASSERT(depth <= (maxSize / sizeof(LocklessSlot)));
106  FW_ASSERT(depth <= (maxSize / messageSize));
107 
108  // The modular comparison in isCandidatePreferred is exact only while queued equal-priority
109  // messages span less than half the U32 sequence domain (SDD section 7); depth bounds message
110  // count, not sequence spread.
111  FW_ASSERT(depth < (std::numeric_limits<U32>::max() / 2));
112 
115 
116  LocklessSlot* slots = nullptr;
117  void* slotsAllocation = nullptr;
118  U8* data = nullptr;
120 
121  // Allocate the slot array. Request padding for manual alignment: allocators are not required
122  // to honor the alignment argument (e.g. Fw::MallocAllocator ignores it).
123  FW_ASSERT(depth * sizeof(LocklessSlot) <= (maxSize - alignof(LocklessSlot)));
124  FwSizeType slotBytesRequested = (depth * sizeof(LocklessSlot)) + alignof(LocklessSlot);
125  FwSizeType slotBytesAllocated = slotBytesRequested;
126  slotsAllocation = allocator.allocate(id, slotBytesAllocated, alignof(LocklessSlot));
127  if (slotsAllocation == nullptr) {
128  status = QueueInterface::Status::ALLOCATION_FAILED;
129  } else if (slotBytesAllocated < slotBytesRequested) {
130  allocator.deallocate(id, slotsAllocation);
131  status = QueueInterface::Status::ALLOCATION_FAILED;
132  } else {
133  const PlatformPointerCastType base = reinterpret_cast<PlatformPointerCastType>(slotsAllocation);
134  const PlatformPointerCastType aligned =
135  (base + (alignof(LocklessSlot) - 1)) & ~static_cast<PlatformPointerCastType>(alignof(LocklessSlot) - 1);
136  const FwSizeType offset = static_cast<FwSizeType>(aligned - base);
137  slots = Fw::arrayPlacementNew<LocklessSlot>(
138  Fw::ByteArray(static_cast<U8*>(slotsAllocation) + offset, slotBytesAllocated - offset), depth);
139  }
140 
141  // Allocate the message-data region.
142  if (status == QueueInterface::Status::OP_OK) {
143  FwSizeType dataBytesRequested = depth * messageSize;
144  FwSizeType dataBytesAllocated = dataBytesRequested;
145  void* dataAllocation = allocator.allocate(id, dataBytesAllocated, alignof(U8));
146  if (dataAllocation == nullptr) {
147  Fw::arrayPlacementDestruct<LocklessSlot>(slots, depth);
148  allocator.deallocate(id, slotsAllocation);
149  status = QueueInterface::Status::ALLOCATION_FAILED;
150  } else if (dataBytesAllocated < dataBytesRequested) {
151  Fw::arrayPlacementDestruct<LocklessSlot>(slots, depth);
152  allocator.deallocate(id, slotsAllocation);
153  allocator.deallocate(id, dataAllocation);
154  status = QueueInterface::Status::ALLOCATION_FAILED;
155  } else {
156  data = static_cast<U8*>(dataAllocation);
157  }
158  }
159 
160  // Publish the configured handle once both allocations succeeded.
161  if (status == QueueInterface::Status::OP_OK) {
162  this->m_handle.m_id = id;
163  this->m_handle.m_messageSize = messageSize;
164  this->m_handle.m_depth = depth;
165  this->m_handle.m_slots = slots;
166  this->m_handle.m_slotsAllocation = slotsAllocation;
167  this->m_handle.m_data = data;
168  this->m_handle.m_sequence.store(0, std::memory_order_relaxed);
169  this->m_handle.m_count.store(0, std::memory_order_relaxed);
170  this->m_handle.m_available.store(0, std::memory_order_relaxed);
171  this->m_handle.m_highMark.store(0, std::memory_order_relaxed);
172  }
173  return status;
174 }
175 
177  if (this->m_handle.m_slots != nullptr) {
180  Fw::arrayPlacementDestruct<LocklessSlot>(this->m_handle.m_slots, this->m_handle.m_depth);
181  allocator.deallocate(this->m_handle.m_id, this->m_handle.m_slotsAllocation);
182  if (this->m_handle.m_data != nullptr) {
183  allocator.deallocate(this->m_handle.m_id, this->m_handle.m_data);
184  }
185  this->m_handle.m_slots = nullptr;
186  this->m_handle.m_slotsAllocation = nullptr;
187  this->m_handle.m_data = nullptr;
188  this->m_handle.m_depth = 0;
189  this->m_handle.m_messageSize = 0;
190  this->m_handle.m_count.store(0, std::memory_order_relaxed);
191  this->m_handle.m_available.store(0, std::memory_order_relaxed);
192  this->m_handle.m_highMark.store(0, std::memory_order_relaxed);
193  this->m_handle.m_sequence.store(0, std::memory_order_relaxed);
194  }
195 }
196 
198  FwSizeType size,
199  FwQueuePriorityType priority,
200  QueueInterface::BlockingType blockType) {
201  // Programming-error checks: queue must be created and inputs must be well-formed. These are
202  // preconditions, not untrusted-input checks.
203  FW_ASSERT(this->m_handle.m_slots != nullptr);
204  FW_ASSERT(this->m_handle.m_data != nullptr);
205  FW_ASSERT(buffer != nullptr);
206 
207  // Reject oversized messages without touching the queue.
208  if (size > this->m_handle.m_messageSize) {
209  return QueueInterface::Status::SIZE_MISMATCH;
210  }
211 
212  const FwSizeType depth = this->m_handle.m_depth;
213  const bool blocking = (blockType == QueueInterface::BlockingType::BLOCKING);
214 
215  // Bounded for non-blocking, unbounded for blocking (explicit user contract).
216  for (FwSizeType pass = 0; blocking || (pass < MAX_RETRY_PASSES); pass++) {
217  for (FwSizeType i = 0; i < depth; i++) {
218  LocklessSlot& slot = this->m_handle.m_slots[i];
219  LocklessStateTagType packed = slot.m_stateTag.load(std::memory_order_acquire);
220  if (stateOf(packed) != LOCKLESS_SLOT_FREE) {
221  continue;
222  }
223  const LocklessStateTagType desired = packStateTag(LOCKLESS_SLOT_WRITING, tagOf(packed) + 1);
224  if (slot.m_stateTag.compare_exchange_strong(packed, desired, std::memory_order_acq_rel,
225  std::memory_order_relaxed)) {
226  if (size > 0) {
227  const FwSizeType offset = i * this->m_handle.m_messageSize;
228  static_cast<void>(::memcpy(this->m_handle.m_data + offset, buffer, static_cast<size_t>(size)));
229  }
230  slot.m_size = size;
231  slot.m_priority.store(priority, std::memory_order_relaxed);
232  slot.m_sequence.store(this->m_handle.m_sequence.fetch_add(1, std::memory_order_relaxed),
233  std::memory_order_relaxed);
234 
235  // Increment both counters *before* publishing READY. This guarantees that a
236  // consumer's decrement (which can only follow a READY observation) never races
237  // ahead of the matching increment, so neither m_count nor m_available can
238  // transiently underflow or wrap to a huge value (fixes GitHub issue #6055).
239  const U32 nextCount = this->m_handle.m_count.fetch_add(1, std::memory_order_acq_rel) + 1;
240  static_cast<void>(this->m_handle.m_available.fetch_add(1, std::memory_order_acq_rel));
241 
242  slot.m_stateTag.store(packStateTag(LOCKLESS_SLOT_READY, tagOf(desired) + 1), std::memory_order_release);
243 
244  // Raise the high-water mark after publication so the message is never invisible
245  // while the producer runs this loop. Each strong-CAS failure strictly raises
246  // prevMark (mark only increases, capped at depth), so at most depth iterations run.
247  U32 prevMark = this->m_handle.m_highMark.load(std::memory_order_relaxed);
248  for (FwSizeType markPass = 0; (markPass < depth) && (nextCount > prevMark); markPass++) {
249  if (this->m_handle.m_highMark.compare_exchange_strong(
250  prevMark, nextCount, std::memory_order_relaxed, std::memory_order_relaxed)) {
251  break;
252  }
253  }
255  }
256  }
257  // No free slot found this pass. Blocking callers back off; non-blocking callers retry.
258  if (blocking) {
260  }
261  }
262  return QueueInterface::Status::FULL;
263 }
264 
266  FwSizeType capacity,
268  FwSizeType& actualSize,
269  FwQueuePriorityType& priority) {
270  // Programming-error checks. These mirror the assertions in send().
271  FW_ASSERT(this->m_handle.m_slots != nullptr);
272  FW_ASSERT(this->m_handle.m_data != nullptr);
273  FW_ASSERT(destination != nullptr);
274 
275  // Reject undersized buffers before claiming a slot so the message stays queued.
276  if (capacity < this->m_handle.m_messageSize) {
277  return QueueInterface::Status::SIZE_MISMATCH;
278  }
279 
280  const FwSizeType depth = this->m_handle.m_depth;
281  const bool blocking = (blockType == QueueInterface::BlockingType::BLOCKING);
282 
283  // Bounded for non-blocking, unbounded for blocking (explicit user contract).
284  for (FwSizeType pass = 0; blocking || (pass < MAX_RETRY_PASSES); pass++) {
285  // Fast path: skip the O(depth) scan while no message is receivable.
286  if (this->m_handle.m_available.load(std::memory_order_acquire) == 0) {
287  if (blocking) {
289  }
290  continue;
291  }
292  FwSizeType bestIndex = depth;
293  FwQueuePriorityType bestPriority = FwQueuePriorityType();
294  U32 bestSequence = 0;
295  LocklessStateTagType bestPacked = 0;
296 
297  for (FwSizeType i = 0; i < depth; i++) {
298  LocklessSlot& slot = this->m_handle.m_slots[i];
299  LocklessStateTagType packed = slot.m_stateTag.load(std::memory_order_acquire);
300  if (stateOf(packed) != LOCKLESS_SLOT_READY) {
301  continue;
302  }
303  const FwQueuePriorityType candidatePriority = slot.m_priority.load(std::memory_order_relaxed);
304  const U32 candidateSequence = slot.m_sequence.load(std::memory_order_relaxed);
305  // Recheck: if the state-tag changed, the relaxed priority/sequence reads above may
306  // belong to a recycled slot; discard the candidate and keep scanning.
307  const LocklessStateTagType packedRecheck = slot.m_stateTag.load(std::memory_order_acquire);
308  if (packed != packedRecheck) {
309  continue;
310  }
311  if ((bestIndex == depth) ||
312  isCandidatePreferred(candidatePriority, candidateSequence, bestPriority, bestSequence)) {
313  bestIndex = i;
314  bestPriority = candidatePriority;
315  bestSequence = candidateSequence;
316  bestPacked = packed;
317  }
318  }
319 
320  if (bestIndex == depth) {
321  if (blocking) {
323  }
324  continue;
325  }
326 
327  const LocklessStateTagType desired = packStateTag(LOCKLESS_SLOT_READING, tagOf(bestPacked) + 1);
328  if (this->m_handle.m_slots[bestIndex].m_stateTag.compare_exchange_strong(
329  bestPacked, desired, std::memory_order_acq_rel, std::memory_order_relaxed)) {
330  // Decrement the receivable count at the successful READY->READING claim. Because
331  // m_available is incremented before READY is published, it is always >= the number
332  // of genuinely READY slots, so this decrement can never underflow.
333  static_cast<void>(this->m_handle.m_available.fetch_sub(1, std::memory_order_acq_rel));
334  LocklessSlot& slot = this->m_handle.m_slots[bestIndex];
335  const FwSizeType storedSize = slot.m_size;
336  FW_ASSERT(storedSize <= capacity);
337  if (storedSize > 0) {
338  const FwSizeType offset = bestIndex * this->m_handle.m_messageSize;
339  static_cast<void>(
340  ::memcpy(destination, this->m_handle.m_data + offset, static_cast<size_t>(storedSize)));
341  }
342  actualSize = storedSize;
343  priority = slot.m_priority.load(std::memory_order_relaxed);
344  // Decrement the occupancy count *before* releasing the slot to FREE. This keeps
345  // m_count (and therefore the high-water mark) at or below the queue depth: a producer
346  // can only re-claim and re-count this slot after observing FREE, which follows the
347  // decrement.
348  static_cast<void>(this->m_handle.m_count.fetch_sub(1, std::memory_order_acq_rel));
349  slot.m_stateTag.store(packStateTag(LOCKLESS_SLOT_FREE, tagOf(desired) + 1), std::memory_order_release);
351  }
352  // CAS failed — another consumer claimed this slot; yield and rescan.
353  if (blocking) {
354  static_cast<void>(Os::Task::delay(Fw::TimeInterval(0, 0)));
355  }
356  }
357  return QueueInterface::Status::EMPTY;
358 }
359 
361  return static_cast<FwSizeType>(this->m_handle.m_available.load(std::memory_order_acquire));
362 }
363 
365  return static_cast<FwSizeType>(this->m_handle.m_highMark.load(std::memory_order_acquire));
366 }
367 
369  return &this->m_handle;
370 }
371 
372 } // namespace Generic
373 } // namespace Os
std::atomic< U32 > m_count
Occupancy count (claimed-or-queued slots) used only for the high-water mark.
virtual void * allocate(const FwEnumStoreType identifier, FwSizeType &size, bool &recoverable, FwSizeType alignment=alignof(std::max_align_t))=0
Operation succeeded.
Definition: Os.hpp:27
PlatformSizeType FwSizeType
I32 FwEnumStoreType
slot contains no data; available to a producer
Status
status returned from the queue send function
Definition: Queue.hpp:30
LocklessSlot * m_slots
Pre-allocated array of m_depth slots, aligned within m_slotsAllocation.
consumer has reserved the slot and is draining it
QueueHandle parent class.
Definition: Queue.hpp:19
FwSizeType m_depth
Configured queue depth in messages.
Status receive(U8 *destination, FwSizeType capacity, BlockingType blockType, FwSizeType &actualSize, FwQueuePriorityType &priority) override
receive a message from the queue
FwSizeType getMessageHighWaterMark() const override
get the maximum number of messages that have been queued at once
static MemAllocatorRegistry & getInstance()
get the singleton registry
static constexpr LocklessStateTagType STATE_MASK
Mask for the state portion of m_stateTag.
Status create(FwEnumStoreType id, const Fw::ConstStringBase &name, FwSizeType depth, FwSizeType messageSize) override
create queue storage
std::atomic< FwQueuePriorityType > m_priority
Stored message priority. Atomic because consumers read it during the scan phase.
static bool isCandidatePreferred(FwQueuePriorityType candidatePriority, U32 candidateSequence, FwQueuePriorityType bestPriority, U32 bestSequence)
decide whether a candidate (priority, sequence) is preferred over the current best ...
LocklessPriorityQueueHandle m_handle
Persistent queue state.
REQUIRED: required for Os::Queue memory allocation when using queues that allocate memory...
MemAllocator & getAnAllocator(const MemoryAllocation::MemoryAllocatorType type)
per-slot data for the lockless priority queue
void * m_slotsAllocation
Raw allocation backing m_slots; retained because allocators may ignore alignment. ...
Status send(const U8 *buffer, FwSizeType size, FwQueuePriorityType priority, BlockingType blockType) override
send a message into the queue
std::atomic< U32 > m_sequence
Sequence assigned to messages on publication for FIFO tiebreak; may wrap (compared modularly)...
LocklessSlot()
Construct a slot in the FREE state with a zero tag.
static constexpr U32 STATE_BITS
Number of low bits used for the state value within m_stateTag.
A variable-length byte array.
Definition: ByteArray.hpp:23
std::atomic< LocklessStateTagType > m_stateTag
FwSizeType getMessagesAvailable() const override
get number of messages currently receivable
uint8_t U8
8-bit unsigned integer
Definition: BasicTypes.h:54
BlockingType
message type
Definition: Queue.hpp:46
producer has reserved the slot and is filling it
PlatformQueuePriorityType FwQueuePriorityType
The type of queue priorities used.
Memory Allocation base class.
constexpr U32 LOCKLESS_QUEUE_BLOCKING_BACKOFF_US
void teardown() override
tear down the queue
A read-only abstract superclass for StringBase.
std::atomic< U32 > m_highMark
Maximum value m_count has ever held. Updated by producers via a bounded CAS loop. ...
static Status delay(const Fw::TimeInterval &interval)
delay the current task
Definition: Task.cpp:204
QueueHandle * getHandle() override
return the underlying queue handle
LocklessPriorityQueueHandle()
Default-construct a handle in the uncreated state.
Defines a base class for a memory allocator for classes.
FwEnumStoreType m_id
Identifier passed to the memory allocator at create() time and reused at teardown().
slot contains a published message available to a consumer
static constexpr FwSizeType MAX_RETRY_PASSES
virtual void deallocate(const FwEnumStoreType identifier, void *ptr)=0
U8 * m_data
Pre-allocated array of m_depth * m_messageSize bytes for message payloads.
FwSizeType m_messageSize
Configured maximum size of a single message.
#define FW_ASSERT(...)
Definition: Assert.hpp:14