py/scheduler: Only run scheduler callbacks queued before run started.

Without this change, a scheduler callback which itself queues a new
callback will have that callback executed as part of the same scheduler
run. Where a callback may re-queue itself, this can lead to an infinite
loop.

With this change, each call to mp_handle_pending() will only service the
callbacks which were queued when the scheduler pass started - any callbacks
added during the run are serviced on the next mp_handle_pending().

This does mean some interrupts may have higher latency (as callback is
deferred until next scheduler run), but the worst-case latency should stay
very similar.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
This commit is contained in:
Angus Gratton
2025-05-23 14:39:37 +10:00
committed by Damien George
parent b15348415e
commit 7f274c7550

View File

@@ -88,17 +88,21 @@ static inline void mp_sched_run_pending(void) {
#if MICROPY_SCHEDULER_STATIC_NODES #if MICROPY_SCHEDULER_STATIC_NODES
// Run all pending C callbacks. // Run all pending C callbacks.
while (MP_STATE_VM(sched_head) != NULL) { mp_sched_node_t *original_tail = MP_STATE_VM(sched_tail);
mp_sched_node_t *node = MP_STATE_VM(sched_head); if (original_tail != NULL) {
MP_STATE_VM(sched_head) = node->next; mp_sched_node_t *node;
if (MP_STATE_VM(sched_head) == NULL) { do {
MP_STATE_VM(sched_tail) = NULL; node = MP_STATE_VM(sched_head);
} MP_STATE_VM(sched_head) = node->next;
mp_sched_callback_t callback = node->callback; if (MP_STATE_VM(sched_head) == NULL) {
node->callback = NULL; MP_STATE_VM(sched_tail) = NULL;
MICROPY_END_ATOMIC_SECTION(atomic_state); }
callback(node); mp_sched_callback_t callback = node->callback;
atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); node->callback = NULL;
MICROPY_END_ATOMIC_SECTION(atomic_state);
callback(node);
atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
} while (node != original_tail); // Don't execute any callbacks scheduled during this run
} }
#endif #endif