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 the occupancy count *before* publishing READY. This guarantees a
236  // consumer's decrement (which can only follow a READY observation) never precedes
237  // this increment, so m_count cannot transiently underflow.
238  const U32 nextCount = this->m_handle.m_count.fetch_add(1, std::memory_order_acq_rel) + 1;
239 
240  slot.m_stateTag.store(packStateTag(LOCKLESS_SLOT_READY, tagOf(desired) + 1), std::memory_order_release);
241 
242  // Increment the receivable count only after READY is published, so a nonzero
243  // getMessagesAvailable() implies at least one message has been made receivable.
244  static_cast<void>(this->m_handle.m_available.fetch_add(1, std::memory_order_acq_rel));
245 
246  // Raise the high-water mark after publication so the message is never invisible
247  // while the producer runs this loop. Each strong-CAS failure strictly raises
248  // prevMark (mark only increases, capped at depth), so at most depth iterations run.
249  U32 prevMark = this->m_handle.m_highMark.load(std::memory_order_relaxed);
250  for (FwSizeType markPass = 0; (markPass < depth) && (nextCount > prevMark); markPass++) {
251  if (this->m_handle.m_highMark.compare_exchange_strong(
252  prevMark, nextCount, std::memory_order_relaxed, std::memory_order_relaxed)) {
253  break;
254  }
255  }
257  }
258  }
259  // No free slot found this pass. Blocking callers back off; non-blocking callers retry.
260  if (blocking) {
262  }
263  }
264  return QueueInterface::Status::FULL;
265 }
266 
268  FwSizeType capacity,
270  FwSizeType& actualSize,
271  FwQueuePriorityType& priority) {
272  // Programming-error checks. These mirror the assertions in send().
273  FW_ASSERT(this->m_handle.m_slots != nullptr);
274  FW_ASSERT(this->m_handle.m_data != nullptr);
275  FW_ASSERT(destination != nullptr);
276 
277  const FwSizeType depth = this->m_handle.m_depth;
278  const bool blocking = (blockType == QueueInterface::BlockingType::BLOCKING);
279 
280  // Bounded for non-blocking, unbounded for blocking (explicit user contract).
281  for (FwSizeType pass = 0; blocking || (pass < MAX_RETRY_PASSES); pass++) {
282  // Fast path: skip the O(depth) scan while no message is receivable.
283  if (this->m_handle.m_available.load(std::memory_order_acquire) == 0) {
284  if (blocking) {
286  }
287  continue;
288  }
289  FwSizeType bestIndex = depth;
290  FwQueuePriorityType bestPriority = FwQueuePriorityType();
291  U32 bestSequence = 0;
292  LocklessStateTagType bestPacked = 0;
293 
294  for (FwSizeType i = 0; i < depth; i++) {
295  LocklessSlot& slot = this->m_handle.m_slots[i];
296  LocklessStateTagType packed = slot.m_stateTag.load(std::memory_order_acquire);
297  if (stateOf(packed) != LOCKLESS_SLOT_READY) {
298  continue;
299  }
300  const FwQueuePriorityType candidatePriority = slot.m_priority.load(std::memory_order_relaxed);
301  const U32 candidateSequence = slot.m_sequence.load(std::memory_order_relaxed);
302  // Recheck: if the state-tag changed, the relaxed priority/sequence reads above may
303  // belong to a recycled slot; discard the candidate and keep scanning.
304  const LocklessStateTagType packedRecheck = slot.m_stateTag.load(std::memory_order_acquire);
305  if (packed != packedRecheck) {
306  continue;
307  }
308  if ((bestIndex == depth) ||
309  isCandidatePreferred(candidatePriority, candidateSequence, bestPriority, bestSequence)) {
310  bestIndex = i;
311  bestPriority = candidatePriority;
312  bestSequence = candidateSequence;
313  bestPacked = packed;
314  }
315  }
316 
317  if (bestIndex == depth) {
318  if (blocking) {
320  }
321  continue;
322  }
323 
324  const LocklessStateTagType desired = packStateTag(LOCKLESS_SLOT_READING, tagOf(bestPacked) + 1);
325  if (this->m_handle.m_slots[bestIndex].m_stateTag.compare_exchange_strong(
326  bestPacked, desired, std::memory_order_acq_rel, std::memory_order_relaxed)) {
327  // Decrement the receivable count at the successful READY->READING claim: this
328  // message can no longer complete another receive.
329  static_cast<void>(this->m_handle.m_available.fetch_sub(1, std::memory_order_acq_rel));
330  LocklessSlot& slot = this->m_handle.m_slots[bestIndex];
331  const FwSizeType storedSize = slot.m_size;
332  FW_ASSERT(storedSize <= capacity);
333  if (storedSize > 0) {
334  const FwSizeType offset = bestIndex * this->m_handle.m_messageSize;
335  static_cast<void>(
336  ::memcpy(destination, this->m_handle.m_data + offset, static_cast<size_t>(storedSize)));
337  }
338  actualSize = storedSize;
339  priority = slot.m_priority.load(std::memory_order_relaxed);
340  // Decrement the occupancy count *before* releasing the slot to FREE. This keeps
341  // m_count (and therefore the high-water mark) at or below the queue depth: a producer
342  // can only re-claim and re-count this slot after observing FREE, which follows the
343  // decrement.
344  static_cast<void>(this->m_handle.m_count.fetch_sub(1, std::memory_order_acq_rel));
345  slot.m_stateTag.store(packStateTag(LOCKLESS_SLOT_FREE, tagOf(desired) + 1), std::memory_order_release);
347  }
348  // CAS failed — another consumer claimed this slot; yield and rescan.
349  if (blocking) {
350  static_cast<void>(Os::Task::delay(Fw::TimeInterval(0, 0)));
351  }
352  }
353  return QueueInterface::Status::EMPTY;
354 }
355 
357  return static_cast<FwSizeType>(this->m_handle.m_available.load(std::memory_order_acquire));
358 }
359 
361  return static_cast<FwSizeType>(this->m_handle.m_highMark.load(std::memory_order_acquire));
362 }
363 
365  return &this->m_handle;
366 }
367 
368 } // namespace Generic
369 } // 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