Skip to content

Commit 337742f

Browse files
projectgusdpgeorge
authored andcommitted
esp32/mpthreadport: Fix uneven GIL allocation between Python threads.
Explicitly yield each time a thread mutex is unlocked. Key to understanding this bug is that Python threads run at equal RTOS priority, and although ESP-IDF FreeRTOS (and I think vanilla FreeRTOS) scheduler will round-robin equal priority tasks in the ready state it does not make a similar guarantee for tasks moving between ready and waiting. The pathological case of this bug is when one Python thread task is busy (i.e. never blocks) it will hog the CPU more than expected, sometimes for an unbounded amount of time. This happens even though it periodically unlocks the GIL to allow another task to run. Assume T1 is busy and T2 is blocked waiting for the GIL. T1 is executing and hits a condition to yield execution: 1. T1 calls MP_THREAD_GIL_EXIT 2. FreeRTOS sees T2 is waiting for the GIL and moves it to the Ready list (but does not preempt, as T2 is same priority, so T1 keeps running). 3. T1 immediately calls MP_THREAD_GIL_ENTER and re-takes the GIL. 4. Pre-emptive context switch happens, T2 wakes up, sees GIL is not available, and goes on the waiting list for the GIL again. To break this cycle step 4 must happen before step 3, but this may be a very narrow window of time so it may not happen regularly - and quantisation of the timing of the tick interrupt to trigger a context switch may mean it never happens. Yielding at the end of step 2 maximises the chance for another task to run. Adds a test that fails on esp32 before this fix and passes afterwards. Fixes issue micropython#15423. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <[email protected]>
1 parent 2994354 commit 337742f

File tree

3 files changed

+60
-0
lines changed

3 files changed

+60
-0
lines changed

ports/esp32/mpthreadport.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ int mp_thread_mutex_lock(mp_thread_mutex_t *mutex, int wait) {
221221

222222
void mp_thread_mutex_unlock(mp_thread_mutex_t *mutex) {
223223
xSemaphoreGive(mutex->handle);
224+
// Python threads run at equal priority, so pre-emptively yield here to
225+
// prevent pathological imbalances where a thread unlocks and then
226+
// immediately re-locks a mutex before a context switch can occur, leaving
227+
// another thread waiting for an unbounded period of time.
228+
taskYIELD();
224229
}
225230

226231
void mp_thread_deinit(void) {

tests/thread/thread_coop.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Threads should be semi-cooperative, to the point where one busy
2+
# thread can't starve out another.
3+
#
4+
# (Note on ports without the GIL this one should always be true, on ports with GIL it's
5+
# a test of the GIL behaviour.)
6+
7+
import _thread
8+
import sys
9+
from time import ticks_ms, ticks_diff, sleep_ms
10+
11+
12+
done = False
13+
14+
ITERATIONS = 5
15+
SLEEP_MS = 250
16+
MAX_DELTA = 30
17+
18+
if sys.platform in ("win32", "linux", "darwin"):
19+
# Conventional operating systems get looser timing restrictions
20+
SLEEP_MS = 300
21+
MAX_DELTA = 100
22+
23+
24+
def busy_thread():
25+
while not done:
26+
pass
27+
28+
29+
def test_sleeps():
30+
global done
31+
ok = True
32+
for _ in range(ITERATIONS):
33+
t0 = ticks_ms()
34+
sleep_ms(SLEEP_MS)
35+
t1 = ticks_ms()
36+
d = ticks_diff(t1, t0)
37+
if d < SLEEP_MS - MAX_DELTA or d > SLEEP_MS + MAX_DELTA:
38+
print("Slept too long ", d)
39+
ok = False
40+
print("OK" if ok else "Not OK")
41+
done = True
42+
43+
44+
# make the thread the busy one, and check sleep time on main task
45+
_thread.start_new_thread(busy_thread, ())
46+
test_sleeps()
47+
48+
sleep_ms(100)
49+
done = False
50+
51+
# now swap them
52+
_thread.start_new_thread(test_sleeps, ())
53+
busy_thread()

tests/thread/thread_coop.py.exp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
OK
2+
OK

0 commit comments

Comments
 (0)