gh-128340: add thread safe handle for loop.call_soon_threadsafe (#128369)

Adds `_ThreadSafeHandle` to be used for callbacks scheduled with `loop.call_soon_threadsafe`.
This commit is contained in:
Kumar Aditya 2025-01-06 18:05:11 +05:30 committed by GitHub
parent 657d7b77e5
commit 7e8c571604
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 151 additions and 1 deletions

View file

@ -873,7 +873,10 @@ class BaseEventLoop(events.AbstractEventLoop):
self._check_closed() self._check_closed()
if self._debug: if self._debug:
self._check_callback(callback, 'call_soon_threadsafe') self._check_callback(callback, 'call_soon_threadsafe')
handle = self._call_soon(callback, args, context) handle = events._ThreadSafeHandle(callback, args, self, context)
self._ready.append(handle)
if handle._source_traceback:
del handle._source_traceback[-1]
if handle._source_traceback: if handle._source_traceback:
del handle._source_traceback[-1] del handle._source_traceback[-1]
self._write_to_self() self._write_to_self()

View file

@ -113,6 +113,34 @@ class Handle:
self._loop.call_exception_handler(context) self._loop.call_exception_handler(context)
self = None # Needed to break cycles when an exception occurs. self = None # Needed to break cycles when an exception occurs.
# _ThreadSafeHandle is used for callbacks scheduled with call_soon_threadsafe
# and is thread safe unlike Handle which is not thread safe.
class _ThreadSafeHandle(Handle):
__slots__ = ('_lock',)
def __init__(self, callback, args, loop, context=None):
super().__init__(callback, args, loop, context)
self._lock = threading.RLock()
def cancel(self):
with self._lock:
return super().cancel()
def cancelled(self):
with self._lock:
return super().cancelled()
def _run(self):
# The event loop checks for cancellation without holding the lock
# It is possible that the handle is cancelled after the check
# but before the callback is called so check it again after acquiring
# the lock and return without calling the callback if it is cancelled.
with self._lock:
if self._cancelled:
return
return super()._run()
class TimerHandle(Handle): class TimerHandle(Handle):
"""Object returned by timed callback registration methods.""" """Object returned by timed callback registration methods."""

View file

@ -353,6 +353,124 @@ class EventLoopTestsMixin:
t.join() t.join()
self.assertEqual(results, ['hello', 'world']) self.assertEqual(results, ['hello', 'world'])
def test_call_soon_threadsafe_handle_block_check_cancelled(self):
results = []
callback_started = threading.Event()
callback_finished = threading.Event()
def callback(arg):
callback_started.set()
results.append(arg)
time.sleep(1)
callback_finished.set()
def run_in_thread():
handle = self.loop.call_soon_threadsafe(callback, 'hello')
self.assertIsInstance(handle, events._ThreadSafeHandle)
callback_started.wait()
# callback started so it should block checking for cancellation
# until it finishes
self.assertFalse(handle.cancelled())
self.assertTrue(callback_finished.is_set())
self.loop.call_soon_threadsafe(self.loop.stop)
t = threading.Thread(target=run_in_thread)
t.start()
self.loop.run_forever()
t.join()
self.assertEqual(results, ['hello'])
def test_call_soon_threadsafe_handle_block_cancellation(self):
results = []
callback_started = threading.Event()
callback_finished = threading.Event()
def callback(arg):
callback_started.set()
results.append(arg)
time.sleep(1)
callback_finished.set()
def run_in_thread():
handle = self.loop.call_soon_threadsafe(callback, 'hello')
self.assertIsInstance(handle, events._ThreadSafeHandle)
callback_started.wait()
# callback started so it cannot be cancelled from other thread until
# it finishes
handle.cancel()
self.assertTrue(callback_finished.is_set())
self.loop.call_soon_threadsafe(self.loop.stop)
t = threading.Thread(target=run_in_thread)
t.start()
self.loop.run_forever()
t.join()
self.assertEqual(results, ['hello'])
def test_call_soon_threadsafe_handle_cancel_same_thread(self):
results = []
callback_started = threading.Event()
callback_finished = threading.Event()
fut = concurrent.futures.Future()
def callback(arg):
callback_started.set()
handle = fut.result()
handle.cancel()
results.append(arg)
callback_finished.set()
self.loop.stop()
def run_in_thread():
handle = self.loop.call_soon_threadsafe(callback, 'hello')
fut.set_result(handle)
self.assertIsInstance(handle, events._ThreadSafeHandle)
callback_started.wait()
# callback cancels itself from same thread so it has no effect
# it runs to completion
self.assertTrue(handle.cancelled())
self.assertTrue(callback_finished.is_set())
self.loop.call_soon_threadsafe(self.loop.stop)
t = threading.Thread(target=run_in_thread)
t.start()
self.loop.run_forever()
t.join()
self.assertEqual(results, ['hello'])
def test_call_soon_threadsafe_handle_cancel_other_thread(self):
results = []
ev = threading.Event()
callback_finished = threading.Event()
def callback(arg):
results.append(arg)
callback_finished.set()
self.loop.stop()
def run_in_thread():
handle = self.loop.call_soon_threadsafe(callback, 'hello')
# handle can be cancelled from other thread if not started yet
self.assertIsInstance(handle, events._ThreadSafeHandle)
handle.cancel()
self.assertTrue(handle.cancelled())
self.assertFalse(callback_finished.is_set())
ev.set()
self.loop.call_soon_threadsafe(self.loop.stop)
# block the main loop until the callback is added and cancelled in the
# other thread
self.loop.call_soon(ev.wait)
t = threading.Thread(target=run_in_thread)
t.start()
self.loop.run_forever()
t.join()
self.assertEqual(results, [])
self.assertFalse(callback_finished.is_set())
def test_call_soon_threadsafe_same_thread(self): def test_call_soon_threadsafe_same_thread(self):
results = [] results = []

View file

@ -0,0 +1 @@
Add internal thread safe handle to be used in :meth:`asyncio.loop.call_soon_threadsafe` for thread safe cancellation.