TL;DR: Do NOT use async-executor or edge-executor V <= 0.4.1 because the queues they use (crossbeam-queue and concurrent-queue) do NOT work with multi-prio threads and lead to priority inversion).
Until (or if) the problem in async-executor is confirmed and fixed, consider using:
embassy-executor
edge-executor from GIT
- The executor from the
futures crate
See this for more details.
====================================
I've been hitting intermittent deadlocks when using the embassy-time-driver feature with async-executor. After a lot of funny watchdog resets, firmware hangs and jtag debugging, I was able to trace it back to a priority inversion between the ESP-IDF timer task and the async executor.
The Problem
The timer callback in embassy_time_driver runs on the ESP-IDF timer task (high priority) and directly calls schedule_next_expiration(), which eventually invokes waker.wake() for expired timers.
These wakers originate from async-executor, which internally uses concurrent_queue for its task queue.
When the executor (running at normal priority) is in the middle of a concurrent_queue::push() operation and a timer fires, the following sequence occurs:
- The executor is mid-push to
concurrent_queue (at a block boundary in the queue's internal state).
- The timer task preempts and runs the callback.
- The callback calls
waker.wake(), which attempts to push to the same concurrent_queue.
concurrent_queue detects contention and calls busy_wait(), which invokes std::thread::yield_now().
- On FreeRTOS,
yield_now() maps to vTaskDelay(0), which only yields to equal or higher priority tasks.
- The lower-priority executor never resumes execution, resulting in a deadlock.
Relevant Code
|
.timer(move || { |
|
static_self |
|
.inner |
|
.lock() |
|
.borrow_mut() |
|
.schedule_next_expiration() |
The schedule_next_expiration() call chains into embassy_time_queue_utils::Queue::next_expiration(), which calls waker.wake() for each expired timer.
Environment
- ESP32 (dual-core; reproduces on single-core as well)
async-executor as the runtime
- Heavy timer usage (multiple concurrent
Timer::after() calls)
Potential Fix Direction
The timer callback should not call waker.wake() directly, since it executes at high priority. A possible approach:
- The timer callback signals a semaphore and returns immediately.
- A helper task, running at the same priority as the executor, waits on the semaphore and calls
schedule_next_expiration().
- With equal priorities,
vTaskDelay(0) should correctly yield between tasks.
Not sure if there's a simpler fix, if you have an idea please let me know...
I think this is the same problem as #619
TL;DR: Do NOT use
async-executororedge-executor V <= 0.4.1because the queues they use (crossbeam-queueandconcurrent-queue) do NOT work with multi-prio threads and lead to priority inversion).Until (or if) the problem in
async-executoris confirmed and fixed, consider using:embassy-executoredge-executorfrom GITfuturescrateSee this for more details.
====================================
I've been hitting intermittent deadlocks when using the
embassy-time-driverfeature withasync-executor. After a lot of funny watchdog resets, firmware hangs and jtag debugging, I was able to trace it back to a priority inversion between the ESP-IDF timer task and the async executor.The Problem
The timer callback in
embassy_time_driverruns on the ESP-IDF timer task (high priority) and directly callsschedule_next_expiration(), which eventually invokeswaker.wake()for expired timers.These wakers originate from
async-executor, which internally usesconcurrent_queuefor its task queue.When the executor (running at normal priority) is in the middle of a
concurrent_queue::push()operation and a timer fires, the following sequence occurs:concurrent_queue(at a block boundary in the queue's internal state).waker.wake(), which attempts to push to the sameconcurrent_queue.concurrent_queuedetects contention and callsbusy_wait(), which invokesstd::thread::yield_now().yield_now()maps tovTaskDelay(0), which only yields to equal or higher priority tasks.Relevant Code
esp-idf-svc/src/timer.rs
Lines 484 to 489 in 71bb200
The
schedule_next_expiration()call chains intoembassy_time_queue_utils::Queue::next_expiration(), which callswaker.wake()for each expired timer.Environment
async-executoras the runtimeTimer::after()calls)Potential Fix Direction
The timer callback should not call
waker.wake()directly, since it executes at high priority. A possible approach:schedule_next_expiration().vTaskDelay(0)should correctly yield between tasks.Not sure if there's a simpler fix, if you have an idea please let me know...
I think this is the same problem as #619