mirror of
https://github.com/esphome/esphome.git
synced 2026-09-16 01:28:39 +00:00
[scheduler] Replace volatile with __atomic_{load,store}_n on NO_ATOMICS
Copilot review on #15947 flagged that `volatile uint32_t` is not a well-defined concurrent access in the C++ memory model — it prevents the compiler caching/eliding the read, but does not turn a plain cross-thread read/write pair into a defined access. Technically still a formal data race even though aligned 32-bit LDR/STR on ARMv5TE is atomic at the hardware level. Switch the NO_ATOMICS counter reads and writes to GCC's atomic builtins with __ATOMIC_RELAXED: - Readers: __atomic_load_n(&counter, __ATOMIC_RELAXED) - Writers (under lock_): __atomic_store_n(&counter, new_value, __ATOMIC_RELAXED) - Increment/decrement (under lock_): explicit load + compute + __atomic_store_n. Lock_ serialises the load-modify-store against other writers; the atomic ops make the write visible to concurrent readers in the memory model. On ARMv5TE these builtins compile to plain LDR/STR — same codegen as the previous volatile approach, and no libatomic dependency (only RMW builtins like __atomic_fetch_add would need the lib). ATOMICS and SINGLE paths are unchanged.
This commit is contained in:
+37
-28
@@ -525,40 +525,41 @@ class Scheduler {
|
||||
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Fast-path counter for process_to_add() to skip taking the lock when there
|
||||
// is nothing to add. std::atomic on ATOMICS; volatile uint32_t on NO_ATOMICS
|
||||
// (aligned 32-bit reads are atomic on ARMv5TE — BK72xx — and volatile
|
||||
// prevents the compiler caching/eliding the read). On NO_ATOMICS, callers
|
||||
// must hold lock_ for any RMW mutation. Not needed on SINGLE.
|
||||
// is nothing to add. std::atomic on ATOMICS; plain uint32_t on NO_ATOMICS
|
||||
// (BK72xx — ARMv5TE single-core, lacks LDREX/STREX so std::atomic RMW would
|
||||
// require libatomic). Reads use __atomic_load_n(__ATOMIC_RELAXED) on
|
||||
// NO_ATOMICS — compiles to a plain LDR (aligned 32-bit load is naturally
|
||||
// atomic on ARMv5TE) but expresses the concurrent-access intent in the C++
|
||||
// memory model. Writes live behind *_locked_ helpers and must hold lock_.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
std::atomic<uint32_t> to_add_count_{0};
|
||||
#else
|
||||
volatile uint32_t to_add_count_{0};
|
||||
uint32_t to_add_count_{0};
|
||||
#endif
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Fast-path helper for process_to_add() to decide if it can skip the lock.
|
||||
// - SINGLE: direct container check (no concurrent writers).
|
||||
// - ATOMICS: lock-free load of to_add_count_.
|
||||
// - NO_ATOMICS: volatile read. A stale 0 is benign — next call() iteration
|
||||
// observes the update; RMW mutation is still under lock_.
|
||||
bool to_add_empty_() const {
|
||||
#ifdef ESPHOME_THREAD_SINGLE
|
||||
return this->to_add_.empty();
|
||||
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
return this->to_add_count_.load(std::memory_order_relaxed) == 0;
|
||||
#else
|
||||
return this->to_add_count_ == 0;
|
||||
return __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Increment to_add_count_ (no-op on single-threaded platforms)
|
||||
// Increment to_add_count_ (no-op on single-threaded platforms).
|
||||
// On NO_ATOMICS the caller must hold lock_ to serialise the load-modify-store
|
||||
// against other writers; the __atomic_store_n makes the write visible to
|
||||
// concurrent readers in the C++ memory model.
|
||||
void to_add_count_increment_locked_() {
|
||||
#ifdef ESPHOME_THREAD_SINGLE
|
||||
// No counter needed — to_add_empty_() checks the vector directly
|
||||
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
this->to_add_count_.fetch_add(1, std::memory_order_relaxed);
|
||||
#else
|
||||
this->to_add_count_++;
|
||||
__atomic_store_n(&this->to_add_count_, __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED) + 1, __ATOMIC_RELAXED);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -569,7 +570,7 @@ class Scheduler {
|
||||
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
this->to_add_count_.store(0, std::memory_order_relaxed);
|
||||
#else
|
||||
this->to_add_count_ = 0;
|
||||
__atomic_store_n(&this->to_add_count_, 0, __ATOMIC_RELAXED);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -581,11 +582,11 @@ class Scheduler {
|
||||
size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
|
||||
|
||||
// Fast-path counter for process_defer_queue_() to skip lock when nothing to
|
||||
// process. See to_add_count_ above for the volatile rationale on NO_ATOMICS.
|
||||
// process. See to_add_count_ above for the NO_ATOMICS rationale.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
std::atomic<uint32_t> defer_count_{0};
|
||||
#else
|
||||
volatile uint32_t defer_count_{0};
|
||||
uint32_t defer_count_{0};
|
||||
#endif
|
||||
|
||||
bool defer_empty_() const {
|
||||
@@ -593,7 +594,7 @@ class Scheduler {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
return this->defer_count_.load(std::memory_order_relaxed) == 0;
|
||||
#else
|
||||
return this->defer_count_ == 0;
|
||||
return __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -601,7 +602,7 @@ class Scheduler {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->defer_count_.fetch_add(1, std::memory_order_relaxed);
|
||||
#else
|
||||
this->defer_count_++;
|
||||
__atomic_store_n(&this->defer_count_, __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED) + 1, __ATOMIC_RELAXED);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -609,61 +610,69 @@ class Scheduler {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->defer_count_.store(0, std::memory_order_relaxed);
|
||||
#else
|
||||
this->defer_count_ = 0;
|
||||
__atomic_store_n(&this->defer_count_, 0, __ATOMIC_RELAXED);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Counter for items marked for removal. Incremented cross-thread in
|
||||
// cancel_item_locked_(). See to_add_count_ above for the volatile rationale
|
||||
// on NO_ATOMICS.
|
||||
// cancel_item_locked_(). See to_add_count_ above for the NO_ATOMICS
|
||||
// rationale.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
std::atomic<uint32_t> to_remove_{0};
|
||||
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
|
||||
volatile uint32_t to_remove_{0};
|
||||
#else
|
||||
uint32_t to_remove_{0};
|
||||
uint32_t to_remove_{0};
|
||||
#endif
|
||||
|
||||
// Lock-free check if there are items to remove (for fast-path in cleanup_)
|
||||
bool to_remove_empty_() const {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
return this->to_remove_.load(std::memory_order_relaxed) == 0;
|
||||
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
|
||||
return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) == 0;
|
||||
#else
|
||||
return this->to_remove_ == 0;
|
||||
return this->to_remove_ == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void to_remove_add_locked_(uint32_t count) {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->to_remove_.fetch_add(count, std::memory_order_relaxed);
|
||||
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
|
||||
__atomic_store_n(&this->to_remove_, __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) + count, __ATOMIC_RELAXED);
|
||||
#else
|
||||
this->to_remove_ += count;
|
||||
this->to_remove_ += count;
|
||||
#endif
|
||||
}
|
||||
|
||||
void to_remove_decrement_locked_() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->to_remove_.fetch_sub(1, std::memory_order_relaxed);
|
||||
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
|
||||
__atomic_store_n(&this->to_remove_, __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) - 1, __ATOMIC_RELAXED);
|
||||
#else
|
||||
this->to_remove_--;
|
||||
this->to_remove_--;
|
||||
#endif
|
||||
}
|
||||
|
||||
void to_remove_clear_locked_() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->to_remove_.store(0, std::memory_order_relaxed);
|
||||
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
|
||||
__atomic_store_n(&this->to_remove_, 0, __ATOMIC_RELAXED);
|
||||
#else
|
||||
this->to_remove_ = 0;
|
||||
this->to_remove_ = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t to_remove_count_() const {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
return this->to_remove_.load(std::memory_order_relaxed);
|
||||
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
|
||||
return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED);
|
||||
#else
|
||||
return this->to_remove_;
|
||||
return this->to_remove_;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user