1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-02 11:22:24 +01:00

atomic remove

This commit is contained in:
J. Nick Koston
2025-08-17 16:09:57 -04:00
parent daf8ec36ab
commit 5b674dc28c
2 changed files with 85 additions and 5 deletions

View File

@@ -82,7 +82,13 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
item->set_name(name_cstr, !is_static_string); item->set_name(name_cstr, !is_static_string);
item->type = type; item->type = type;
item->callback = std::move(func); item->callback = std::move(func);
// Initialize remove to false (though it should already be from constructor)
// Not using mark_item_removed_ helper since we're setting to false, not true
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
item->remove.store(false, std::memory_order_relaxed);
#else
item->remove = false; item->remove = false;
#endif
item->is_retry = is_retry; item->is_retry = is_retry;
#ifndef ESPHOME_THREAD_SINGLE #ifndef ESPHOME_THREAD_SINGLE
@@ -398,6 +404,31 @@ void HOT Scheduler::call(uint32_t now) {
this->pop_raw_(); this->pop_raw_();
continue; continue;
} }
// Check if item is marked for removal
// This handles two cases:
// 1. Item was marked for removal after cleanup_() but before we got here
// 2. Item is marked for removal but wasn't at the front of the heap during cleanup_()
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
// Multi-threaded platforms without atomics: must take lock to safely read remove flag
{
LockGuard guard{this->lock_};
if (is_item_removed_(item.get())) {
this->pop_raw_();
this->to_remove_--;
continue;
}
}
#else
// Single-threaded or multi-threaded with atomics: can check without lock
if (is_item_removed_(item.get())) {
LockGuard guard{this->lock_};
this->pop_raw_();
this->to_remove_--;
continue;
}
#endif
#ifdef ESPHOME_DEBUG_SCHEDULER #ifdef ESPHOME_DEBUG_SCHEDULER
const char *item_name = item->get_name(); const char *item_name = item->get_name();
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
@@ -518,7 +549,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
if (type == SchedulerItem::TIMEOUT) { if (type == SchedulerItem::TIMEOUT) {
for (auto &item : this->defer_queue_) { for (auto &item : this->defer_queue_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
item->remove = true; this->mark_item_removed_(item.get());
total_cancelled++; total_cancelled++;
} }
} }
@@ -528,7 +559,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
// Cancel items in the main heap // Cancel items in the main heap
for (auto &item : this->items_) { for (auto &item : this->items_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
item->remove = true; this->mark_item_removed_(item.get());
total_cancelled++; total_cancelled++;
this->to_remove_++; // Track removals for heap items this->to_remove_++; // Track removals for heap items
} }
@@ -537,7 +568,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
// Cancel items in to_add_ // Cancel items in to_add_
for (auto &item : this->to_add_) { for (auto &item : this->to_add_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
item->remove = true; this->mark_item_removed_(item.get());
total_cancelled++; total_cancelled++;
// Don't track removals for to_add_ items // Don't track removals for to_add_ items
} }

View File

@@ -97,22 +97,42 @@ class Scheduler {
std::function<void()> callback; std::function<void()> callback;
// Bit-packed fields to minimize padding #ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic for lock-free access
// Place atomic<bool> separately since it can't be packed with bit fields
std::atomic<bool> remove{false};
// Bit-packed fields (3 bits used, 5 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
bool is_retry : 1; // True if this is a retry timeout
// 5 bits padding
#else
// Single-threaded or multi-threaded without atomics: can pack all fields together
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1; bool remove : 1;
bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
bool is_retry : 1; // True if this is a retry timeout bool is_retry : 1; // True if this is a retry timeout
// 4 bits padding // 4 bits padding
#endif
// Constructor // Constructor
SchedulerItem() SchedulerItem()
: component(nullptr), : component(nullptr),
interval(0), interval(0),
next_execution_(0), next_execution_(0),
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// remove is initialized in the member declaration as std::atomic<bool>{false}
type(TIMEOUT),
name_is_dynamic(false),
is_retry(false) {
#else
type(TIMEOUT), type(TIMEOUT),
remove(false), remove(false),
name_is_dynamic(false), name_is_dynamic(false),
is_retry(false) { is_retry(false) {
#endif
name_.static_name = nullptr; name_.static_name = nullptr;
} }
@@ -219,6 +239,35 @@ class Scheduler {
return item->remove || (item->component != nullptr && item->component->is_failed()); return item->remove || (item->component != nullptr && item->component->is_failed());
} }
// Helper to check if item is marked for removal (platform-specific)
// Returns true if item should be skipped, handles platform-specific synchronization
// NOTE: For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller must hold lock!
bool is_item_removed_(SchedulerItem *item) const {
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic load for lock-free access
return item->remove.load(std::memory_order_acquire);
#else
// Single-threaded (ESPHOME_THREAD_SINGLE) or
// multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read
// For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
return item->remove;
#endif
}
// Helper to mark item for removal (platform-specific)
// NOTE: For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller must hold lock!
void mark_item_removed_(SchedulerItem *item) {
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic store
item->remove.store(true, std::memory_order_release);
#else
// Single-threaded (ESPHOME_THREAD_SINGLE) or
// multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
// For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
item->remove = true;
#endif
}
// Template helper to check if any item in a container matches our criteria // Template helper to check if any item in a container matches our criteria
template<typename Container> template<typename Container>
bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,