mirror of
https://github.com/esphome/esphome.git
synced 2026-03-03 19:28:20 -07:00
Replace Mutex with atomic_flag spinlock for MULTI_ATOMICS path
The atomics path only takes the lock during rollover (every ~49.7 days) for a ~5 instruction critical section. A std::atomic_flag spinlock (1 byte) replaces the full FreeRTOS Mutex (~80-100 bytes RAM) since contention is near-zero. The full Mutex is kept only for the MULTI_NO_ATOMICS path (BK72xx) which needs it for broader locking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,10 +21,14 @@ static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max()
|
||||
|
||||
uint64_t Millis64Impl::compute(uint32_t now) {
|
||||
// State variables for rollover tracking - static to persist across calls
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
|
||||
static Mutex lock;
|
||||
#endif
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// Spinlock for rollover serialization (taken only every ~49.7 days).
|
||||
// Uses atomic_flag (1 byte) instead of a full FreeRTOS Mutex (~80-100 bytes)
|
||||
// since the critical section is tiny and contention is near-zero.
|
||||
static std::atomic_flag rollover_lock = ATOMIC_FLAG_INIT;
|
||||
/*
|
||||
* Multi-threaded platforms with atomic support: last_millis needs atomic for lock-free updates.
|
||||
* Writers publish last_millis with memory_order_release and readers use memory_order_acquire.
|
||||
@@ -33,7 +37,7 @@ uint64_t Millis64Impl::compute(uint32_t now) {
|
||||
*/
|
||||
static std::atomic<uint32_t> last_millis{0};
|
||||
/*
|
||||
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding lock;
|
||||
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding spinlock;
|
||||
* read concurrently. Atomic (relaxed) avoids a formal data race. Ordering relative
|
||||
* to last_millis is provided by its release store and the corresponding acquire loads.
|
||||
*/
|
||||
@@ -129,7 +133,7 @@ uint64_t Millis64Impl::compute(uint32_t now) {
|
||||
// Uses atomic operations with acquire/release semantics to ensure coherent
|
||||
// reads of millis_major and last_millis across cores. Features:
|
||||
// 1. Epoch-coherency retry loop to handle concurrent updates
|
||||
// 2. Lock only taken for actual rollover detection and update
|
||||
// 2. Spinlock only taken for actual rollover detection (every ~49.7 days)
|
||||
// 3. Lock-free CAS updates for normal forward time progression
|
||||
// 4. Memory ordering ensures cores see consistent time values
|
||||
|
||||
@@ -144,12 +148,14 @@ uint64_t Millis64Impl::compute(uint32_t now) {
|
||||
*/
|
||||
uint32_t last = last_millis.load(std::memory_order_acquire);
|
||||
|
||||
// If we might be near a rollover (large backwards jump), take the lock for the entire operation
|
||||
// If we might be near a rollover (large backwards jump), take the spinlock
|
||||
// This ensures rollover detection and last_millis update are atomic together
|
||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||
// Potential rollover - need lock for atomic rollover detection + update
|
||||
LockGuard guard{lock};
|
||||
// Re-read with lock held; mutex already provides ordering
|
||||
// Potential rollover - acquire spinlock for atomic rollover detection + update
|
||||
while (rollover_lock.test_and_set(std::memory_order_acquire)) {
|
||||
// Spin; critical section is ~5 instructions, contention near-zero
|
||||
}
|
||||
// Re-read with spinlock held
|
||||
last = last_millis.load(std::memory_order_relaxed);
|
||||
|
||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||
@@ -161,11 +167,12 @@ uint64_t Millis64Impl::compute(uint32_t now) {
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
}
|
||||
/*
|
||||
* Update last_millis while holding the lock to prevent races.
|
||||
* Update last_millis while holding the spinlock to prevent races.
|
||||
* Publish the new low-word *after* bumping millis_major (done above)
|
||||
* so readers never see a mismatched pair.
|
||||
*/
|
||||
last_millis.store(now, std::memory_order_release);
|
||||
rollover_lock.clear(std::memory_order_release);
|
||||
} else {
|
||||
// Normal case: Try lock-free update, but only allow forward movement within same epoch
|
||||
// This prevents accidentally moving backwards across a rollover boundary
|
||||
|
||||
Reference in New Issue
Block a user