ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
lock_free_queue.h
Go to the documentation of this file.
1#pragma once
2
4
5#include <atomic>
6#include <cstddef>
7
8#ifdef USE_ESP32
9#include <freertos/FreeRTOS.h>
10#include <freertos/task.h>
11#endif
12
13/*
14 * Lock-free queue for single-producer single-consumer scenarios.
15 * This allows one thread to push items and another to pop them without
16 * blocking each other.
17 *
18 * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer.
19 * Available on platforms with FreeRTOS support (ESP32, LibreTiny).
20 *
21 * Common use cases:
22 * - BLE events: BLE task produces, main loop consumes
23 * - MQTT messages: main task produces, MQTT thread consumes
24 *
25 * @tparam T The type of elements stored in the queue (must be a pointer type)
26 * @tparam SIZE The maximum number of elements (1-255, limited by uint8_t indices)
27 */
28
29namespace esphome {
30
31namespace lockfree_internal {
32#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
33// Platforms whose cores lack atomic read-modify-write instructions (currently
34// the ARMv5TE BK72xx SoCs — no LDREX/STREX, no libatomic; other LibreTiny
35// chips such as LN882x/RTL87xx are ARMv7-M and keep std::atomic). For this
36// queue's SPSC contract RMW atomics are not needed: aligned 8/16-bit loads and
37// stores are single instructions on these cores, so torn reads cannot occur,
38// and on a single in-order core a compiler barrier supplies all the
39// acquire/release ordering the algorithm requires. Each index has exactly one
40// writer (head_: consumer, tail_: producer). The dropped counter's
41// increment/exchange pair is not atomic here — a concurrent reset can lose
42// counts — which is acceptable for a diagnostic drop counter.
43#define ESPHOME_LFQ_COMPILER_BARRIER() __asm__ __volatile__("" ::: "memory")
44template<typename T> class PlainAtomic {
45 public:
46 PlainAtomic() = default;
47 constexpr PlainAtomic(T value) : value_(value) {}
48 T load(std::memory_order order = std::memory_order_seq_cst) const {
49 T value = value_;
50 if (order != std::memory_order_relaxed)
51 ESPHOME_LFQ_COMPILER_BARRIER(); // acquire: later reads may not hoist above this load
52 return value;
53 }
54 void store(T value, std::memory_order order = std::memory_order_seq_cst) {
55 if (order != std::memory_order_relaxed)
56 ESPHOME_LFQ_COMPILER_BARRIER(); // release: earlier writes may not sink below this store
57 value_ = value;
58 }
59 T fetch_add(T amount, std::memory_order /*order*/ = std::memory_order_seq_cst) {
60 T value = value_;
61 value_ = value + amount;
62 return value;
63 }
64 T exchange(T desired, std::memory_order /*order*/ = std::memory_order_seq_cst) {
65 T value = value_;
66 value_ = desired;
67 return value;
68 }
69
70 private:
71 volatile T value_{0};
72};
73template<typename T> using AtomicIndex = PlainAtomic<T>;
74#else
75template<typename T> using AtomicIndex = std::atomic<T>;
76#endif
77} // namespace lockfree_internal
78
79// Base lock-free queue without task notification
80template<class T, uint8_t SIZE> class LockFreeQueue {
81 public:
83
84 bool push(T *element) {
85 bool was_empty;
86 uint8_t old_tail;
87 return push_internal_(element, was_empty, old_tail);
88 }
89
90 protected:
91 // Advance ring buffer index by one, wrapping at SIZE.
92 // Power-of-2 sizes use modulo (compiler emits single mask instruction).
93 // Non-power-of-2 sizes use comparison to avoid expensive multiply-shift sequences.
94 static constexpr uint8_t next_index(uint8_t index) {
95 if constexpr ((SIZE & (SIZE - 1)) == 0) {
96 return (index + 1) % SIZE;
97 } else {
98 uint8_t next = index + 1;
99 if (next >= SIZE) [[unlikely]]
100 next = 0;
101 return next;
102 }
103 }
104
105 // Internal push that reports queue state - for use by derived classes
106 bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) {
107 if (element == nullptr)
108 return false;
109
110 uint8_t current_tail = tail_.load(std::memory_order_relaxed);
111 uint8_t next_tail = next_index(current_tail);
112
113 // Read head before incrementing tail
114 uint8_t head_before = head_.load(std::memory_order_acquire);
115
116 if (next_tail == head_before) {
117 // Buffer full
118 dropped_count_.fetch_add(1, std::memory_order_relaxed);
119 return false;
120 }
121
122 was_empty = (current_tail == head_before);
123 old_tail = current_tail;
124
125 buffer_[current_tail] = element;
126 tail_.store(next_tail, std::memory_order_release);
127
128 return true;
129 }
130
131 public:
132 T *pop() {
133 uint8_t current_head = head_.load(std::memory_order_relaxed);
134
135 if (current_head == tail_.load(std::memory_order_acquire)) {
136 return nullptr; // Empty
137 }
138
139 T *element = buffer_[current_head];
140 head_.store(next_index(current_head), std::memory_order_release);
141 return element;
142 }
143
144 size_t size() const {
145 uint8_t tail = tail_.load(std::memory_order_acquire);
146 uint8_t head = head_.load(std::memory_order_acquire);
147 if constexpr ((SIZE & (SIZE - 1)) == 0) {
148 return (tail - head + SIZE) % SIZE;
149 } else {
150 int diff = static_cast<int>(tail) - static_cast<int>(head);
151 if (diff < 0)
152 diff += SIZE;
153 return static_cast<size_t>(diff);
154 }
155 }
156
158 // Fast path: relaxed load is a single instruction on all platforms.
159 // The atomic exchange (especially for uint16_t on Xtensa) compiles to
160 // an expensive sub-word CAS retry loop (~25 instructions + memory barriers).
161 // Since drops are rare, avoid the exchange in the common case.
162 if (dropped_count_.load(std::memory_order_relaxed) == 0)
163 return 0;
164 return dropped_count_.exchange(0, std::memory_order_relaxed);
165 }
166
167 void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
168
169 bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
170
171 bool full() const {
172 uint8_t next_tail = next_index(tail_.load(std::memory_order_relaxed));
173 return next_tail == head_.load(std::memory_order_acquire);
174 }
175
176 protected:
177 T *buffer_[SIZE]{};
178 // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
179 lockfree_internal::AtomicIndex<uint16_t> dropped_count_; // 65535 max - more than enough for drop tracking
180 // Atomic: written by consumer (pop), read by producer (push) to check if full
181 // Using uint8_t limits queue size to 255 elements but saves memory and ensures
182 // atomic operations are efficient on all platforms
184 // Atomic: written by producer (push), read by consumer (pop) to check if empty
186};
187
188#ifdef USE_ESP32
189// Extended queue with task notification support
190template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQueue<T, SIZE> {
191 public:
192 NotifyingLockFreeQueue() : LockFreeQueue<T, SIZE>(), task_to_notify_(nullptr) {}
193
194 bool push(T *element) {
195 bool was_empty;
196 uint8_t old_tail;
197 bool result = this->push_internal_(element, was_empty, old_tail);
198
199 // Notify optimization: only notify if we need to
200 if (result && task_to_notify_ != nullptr &&
201 (was_empty || this->head_.load(std::memory_order_acquire) == old_tail)) {
202 // Notify in two cases:
203 // 1. Queue was empty - consumer might be going to sleep
204 // 2. Consumer just caught up to where tail was - might go to sleep
205 // Note: There's a benign race in case 2 - between reading head and calling
206 // xTaskNotifyGive(), the consumer could advance further. This would result
207 // in an unnecessary wake-up, but is harmless and extremely rare in practice.
208 xTaskNotifyGive(task_to_notify_);
209 }
210 // Otherwise: consumer is still behind, no need to notify
211
212 return result;
213 }
214
215 // Set the FreeRTOS task handle to notify when items are pushed to the queue
216 // This enables efficient wake-up of a consumer task that's waiting for data
217 // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications
218 void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; }
219
220 private:
221 TaskHandle_t task_to_notify_;
222};
223#endif
224
225} // namespace esphome
uint16_t get_and_reset_dropped_count()
bool push(T *element)
static constexpr uint8_t next_index(uint8_t index)
lockfree_internal::AtomicIndex< uint8_t > head_
bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail)
lockfree_internal::AtomicIndex< uint16_t > dropped_count_
lockfree_internal::AtomicIndex< uint8_t > tail_
void set_task_to_notify(TaskHandle_t task)
T fetch_add(T amount, std::memory_order=std::memory_order_seq_cst)
T exchange(T desired, std::memory_order=std::memory_order_seq_cst)
T load(std::memory_order order=std::memory_order_seq_cst) const
void store(T value, std::memory_order order=std::memory_order_seq_cst)