Browse Source

shared/tinyusb: Only run TinyUSB on the main thread if GIL is disabled.

If GIL is disabled then there's threat of a race condition if some other
code specifically requests USB processing (i.e. to unblock stdio), while
a scheduled TinyUSB callback is already running on another thread.

Relies on the change in the parent commit, where scheduler is restricted
to main thread if GIL is disabled.

Fixes #15390 - "TinyUSB callback can't recurse" exceptions on rp2 when
using _thread module and USB serial I/O.

Adds a unit test for stdin functioning correctly in threads (fails on rp2
port without this fix).

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
pull/15448/head
Angus Gratton 2 months ago
committed by Damien George
parent
commit
5d8878b582
  1. 9
      shared/tinyusb/mp_usbd_runtime.c
  2. 44
      tests/thread/thread_stdin.py

9
shared/tinyusb/mp_usbd_runtime.c

@ -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 // Task function can be called manually to force processing of USB events
// (mostly from USB-CDC serial port when blocking.) // (mostly from USB-CDC serial port when blocking.)
void mp_usbd_task(void) { 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 (in_usbd_task) {
// If this exception triggers, it means a USB callback tried to do // If this exception triggers, it means a USB callback tried to do
// something that itself became blocked on TinyUSB (most likely: read or // something that itself became blocked on TinyUSB (most likely: read or

44
tests/thread/thread_stdin.py

@ -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…
Cancel
Save