Skip to content

Commit

Permalink
revise and document cancellation semantics
Browse files Browse the repository at this point in the history
in short, cancellable threads always use system tasks. normal threads use the host task, unless passed a token
  • Loading branch information
richardsheridan committed Oct 15, 2023
1 parent eab30c4 commit 2f79f15
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 102 deletions.
13 changes: 12 additions & 1 deletion docs/source/reference-core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1823,9 +1823,20 @@ to spawn a child thread, and then use a :ref:`memory channel

.. literalinclude:: reference-core/from-thread-example.py

.. note::

The ``from_thread.run*`` functions reuse the host task that called
:func:`trio.to_thread.run_sync` to run your provided function in the typical case,
namely when ``cancellable=False`` so Trio can be sure that the task will always be
around to perform the work. If you pass ``cancellable=True`` at the outset, or if
you provide a :class:`~trio.lowlevel.TrioToken` when calling back in to Trio, your
functions will be executed in a new system task. Therefore, the
:func:`~trio.lowlevel.current_task`, :func:`current_effective_deadline`, or other
task-tree specific values may differ depending on keyword argument values.

You can also use :func:`trio.from_thread.check_cancelled` to check for cancellation from
a thread that was spawned by :func:`trio.to_thread.run_sync`. If the call to
:func:`~trio.to_thread.run_sync` was cancelled, then
:func:`~trio.to_thread.run_sync` was cancelled (even if ``cancellable=False``!), then
:func:`~trio.from_thread.check_cancelled` will raise :func:`trio.Cancelled`.
It's like ``trio.from_thread.run(trio.sleep, 0)``, but much faster.

Expand Down
34 changes: 19 additions & 15 deletions trio/_tests/test_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,14 +933,16 @@ async def async_time_bomb():
async def test_from_thread_check_cancelled():
q = stdlib_queue.Queue()

async def child(cancellable):
record.append("start")
try:
return await to_thread_run_sync(f, cancellable=cancellable)
except _core.Cancelled:
record.append("cancel")
finally:
record.append("exit")
async def child(cancellable, scope):
with scope:
record.append("start")
try:
return await to_thread_run_sync(f, cancellable=cancellable)
except _core.Cancelled:
record.append("cancel")
raise
finally:
record.append("exit")

def f():
try:
Expand All @@ -956,7 +958,7 @@ def f():
record = []
ev = threading.Event()
async with _core.open_nursery() as nursery:
nursery.start_soon(child, False)
nursery.start_soon(child, False, _core.CancelScope())
await wait_all_tasks_blocked()
assert record[0] == "start"
assert q.get(timeout=1) == "Not Cancelled"
Expand All @@ -968,14 +970,15 @@ def f():
# the appropriate cancel scope
record = []
ev = threading.Event()
scope = _core.CancelScope() # Nursery cancel scope gives false positives
async with _core.open_nursery() as nursery:
nursery.start_soon(child, False)
nursery.start_soon(child, False, scope)
await wait_all_tasks_blocked()
assert record[0] == "start"
assert q.get(timeout=1) == "Not Cancelled"
nursery.cancel_scope.cancel()
scope.cancel()
ev.set()
assert nursery.cancel_scope.cancelled_caught
assert scope.cancelled_caught
assert "cancel" in record
assert record[-1] == "exit"

Expand All @@ -992,13 +995,14 @@ def f(): # noqa: F811

record = []
ev = threading.Event()
scope = _core.CancelScope()
async with _core.open_nursery() as nursery:
nursery.start_soon(child, True)
nursery.start_soon(child, True, scope)
await wait_all_tasks_blocked()
assert record[0] == "start"
nursery.cancel_scope.cancel()
scope.cancel()
ev.set()
assert nursery.cancel_scope.cancelled_caught
assert scope.cancelled_caught
assert "cancel" in record
assert record[-1] == "exit"
assert q.get(timeout=1) == "Cancelled"
Expand Down
Loading

0 comments on commit 2f79f15

Please sign in to comment.