Skip to content

py/scheduler,rp2: Avoid scheduler race conditions when GIL disabled, fix "TinyUSB callback can't recurse" error #15448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/library/micropython.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ Functions
the heap may be locked) and scheduling a function to call later will lift
those restrictions.

On multi-threaded ports, the scheduled function's behaviour depends on
whether the Global Interpreter Lock (GIL) is enabled for the specific port:

- If GIL is enabled, the function can preempt any thread and run in its
context.
- If GIL is disabled, the function will only preempt the main thread and run
in its context.

Note: If `schedule()` is called from a preempting IRQ, when memory
allocation is not allowed and the callback to be passed to `schedule()` is
a bound method, passing this directly will fail. This is because creating a
Expand Down
8 changes: 7 additions & 1 deletion py/scheduler.c
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,13 @@ void mp_handle_pending(bool raise_exc) {

// Handle any pending callbacks.
#if MICROPY_ENABLE_SCHEDULER
if (MP_STATE_VM(sched_state) == MP_SCHED_PENDING) {
bool run_scheduler = (MP_STATE_VM(sched_state) == MP_SCHED_PENDING);
#if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
// Avoid races by running the scheduler on the main thread, only.
// (Not needed if GIL enabled, as GIL ensures thread safety here.)
run_scheduler = run_scheduler && mp_thread_is_main_thread();
#endif
if (run_scheduler) {
mp_sched_run_pending();
}
#endif
Expand Down
9 changes: 9 additions & 0 deletions shared/tinyusb/mp_usbd_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,15 @@ void mp_usbd_task_callback(mp_sched_node_t *node) {
// Task function can be called manually to force processing of USB events
// (mostly from USB-CDC serial port when blocking.)
void mp_usbd_task(void) {
#if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
if (!mp_thread_is_main_thread()) {
// Avoid race with the scheduler callback by scheduling TinyUSB to run
// on the main thread.
mp_usbd_schedule_task();
return;
}
#endif

if (in_usbd_task) {
// If this exception triggers, it means a USB callback tried to do
// something that itself became blocked on TinyUSB (most likely: read or
Expand Down
44 changes: 44 additions & 0 deletions tests/thread/thread_stdin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Test that having multiple threads block on stdin doesn't cause any issues.
#
# The test doesn't expect any input on stdin.
#
# This is a regression test for https://github.com/micropython/micropython/issues/15230
# on rp2, but doubles as a general property to test across all ports.
import sys
import _thread

try:
import select
except ImportError:
print("SKIP")
raise SystemExit


class StdinWaiter:
def __init__(self):
self._done = False

def wait_stdin(self, timeout_ms):
poller = select.poll()
poller.register(sys.stdin, select.POLLIN)
poller.poll(timeout_ms)
# Ignoring the poll result as we don't expect any input
self._done = True

def is_done(self):
return self._done


thread_waiter = StdinWaiter()
_thread.start_new_thread(thread_waiter.wait_stdin, (1000,))
StdinWaiter().wait_stdin(1000)

# Spinning here is mostly not necessary but there is some inconsistency waking
# the two threads, especially on CPython CI runners where the thread may not
# have run yet. The actual delay is <20ms but spinning here instead of
# sleep(0.1) means the test can run on MP builds without float support.
while not thread_waiter.is_done():
pass

# The background thread should have completed its wait by now.
print(thread_waiter.is_done())
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy