mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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:
parent
657d7b77e5
commit
7e8c571604
4 changed files with 151 additions and 1 deletions
|
@ -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()
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add internal thread safe handle to be used in :meth:`asyncio.loop.call_soon_threadsafe` for thread safe cancellation.
|
Loading…
Add table
Add a link
Reference in a new issue