17
17
from trio ._core ._traps import RaiseCancelT
18
18
19
19
from ._core import (
20
- CancelScope ,
21
20
RunVar ,
22
21
TrioToken ,
23
22
disable_ki_protection ,
@@ -35,6 +34,7 @@ class _ParentTaskData(threading.local):
35
34
parent task of native Trio threads."""
36
35
37
36
token : TrioToken
37
+ abandon_on_cancel : bool
38
38
cancel_register : list [RaiseCancelT | None ]
39
39
task_register : list [trio .lowlevel .Task | None ]
40
40
@@ -74,11 +74,6 @@ class ThreadPlaceholder:
74
74
75
75
76
76
# Types for the to_thread_run_sync message loop
77
- @attr .s (frozen = True , eq = False )
78
- class ThreadDone (Generic [RetT ]):
79
- result : outcome .Outcome [RetT ] = attr .ib ()
80
-
81
-
82
77
@attr .s (frozen = True , eq = False )
83
78
class Run (Generic [RetT ]):
84
79
afn : Callable [..., Awaitable [RetT ]] = attr .ib ()
@@ -87,7 +82,6 @@ class Run(Generic[RetT]):
87
82
queue : stdlib_queue .SimpleQueue [outcome .Outcome [RetT ]] = attr .ib (
88
83
init = False , factory = stdlib_queue .SimpleQueue
89
84
)
90
- scope : CancelScope = attr .ib (init = False , factory = CancelScope )
91
85
92
86
@disable_ki_protection
93
87
async def unprotected_afn (self ) -> RetT :
@@ -108,14 +102,32 @@ async def run(self) -> None:
108
102
await trio .lowlevel .cancel_shielded_checkpoint ()
109
103
110
104
async def run_system (self ) -> None :
111
- # NOTE: There is potential here to only conditionally enter a CancelScope
112
- # when we need it, sparing some computation. But doing so adds substantial
113
- # complexity, so we'll leave it until real need is demonstrated.
114
- with self .scope :
115
- result = await outcome .acapture (self .unprotected_afn )
116
- assert not self .scope .cancelled_caught , "any Cancelled should go to our parent"
105
+ result = await outcome .acapture (self .unprotected_afn )
117
106
self .queue .put_nowait (result )
118
107
108
+ def run_in_host_task (self , token : TrioToken ) -> None :
109
+ task_register = PARENT_TASK_DATA .task_register
110
+
111
+ def in_trio_thread () -> None :
112
+ task = task_register [0 ]
113
+ assert task is not None , "guaranteed by abandon_on_cancel semantics"
114
+ trio .lowlevel .reschedule (task , outcome .Value (self ))
115
+
116
+ token .run_sync_soon (in_trio_thread )
117
+
118
+ def run_in_system_nursery (self , token : TrioToken ) -> None :
119
+ def in_trio_thread () -> None :
120
+ try :
121
+ trio .lowlevel .spawn_system_task (
122
+ self .run , name = self .afn , context = self .context
123
+ )
124
+ except RuntimeError : # system nursery is closed
125
+ self .queue .put_nowait (
126
+ outcome .Error (trio .RunFinishedError ("system nursery is closed" ))
127
+ )
128
+
129
+ token .run_sync_soon (in_trio_thread )
130
+
119
131
120
132
@attr .s (frozen = True , eq = False )
121
133
class RunSync (Generic [RetT ]):
@@ -144,6 +156,19 @@ def run_sync(self) -> None:
144
156
result = outcome .capture (self .context .run , self .unprotected_fn )
145
157
self .queue .put_nowait (result )
146
158
159
+ def run_in_host_task (self , token : TrioToken ) -> None :
160
+ task_register = PARENT_TASK_DATA .task_register
161
+
162
+ def in_trio_thread () -> None :
163
+ task = task_register [0 ]
164
+ assert task is not None , "guaranteed by abandon_on_cancel semantics"
165
+ trio .lowlevel .reschedule (task , outcome .Value (self ))
166
+
167
+ token .run_sync_soon (in_trio_thread )
168
+
169
+ def run_in_system_nursery (self , token : TrioToken ) -> None :
170
+ token .run_sync_soon (self .run_sync )
171
+
147
172
148
173
@enable_ki_protection # Decorator used on function with Coroutine[Any, Any, RetT]
149
174
async def to_thread_run_sync ( # type: ignore[misc]
@@ -237,7 +262,7 @@ async def to_thread_run_sync( # type: ignore[misc]
237
262
238
263
"""
239
264
await trio .lowlevel .checkpoint_if_cancelled ()
240
- cancellable = bool (cancellable ) # raise early if cancellable.__bool__ raises
265
+ abandon_on_cancel = bool (cancellable ) # raise early if cancellable.__bool__ raises
241
266
if limiter is None :
242
267
limiter = current_default_thread_limiter ()
243
268
@@ -266,9 +291,7 @@ def do_release_then_return_result() -> RetT:
266
291
267
292
result = outcome .capture (do_release_then_return_result )
268
293
if task_register [0 ] is not None :
269
- trio .lowlevel .reschedule (
270
- task_register [0 ], outcome .Value (ThreadDone (result ))
271
- )
294
+ trio .lowlevel .reschedule (task_register [0 ], outcome .Value (result ))
272
295
273
296
current_trio_token = trio .lowlevel .current_trio_token ()
274
297
@@ -283,6 +306,7 @@ def worker_fn() -> RetT:
283
306
current_async_library_cvar .set (None )
284
307
285
308
PARENT_TASK_DATA .token = current_trio_token
309
+ PARENT_TASK_DATA .abandon_on_cancel = abandon_on_cancel
286
310
PARENT_TASK_DATA .cancel_register = cancel_register
287
311
PARENT_TASK_DATA .task_register = task_register
288
312
try :
@@ -299,6 +323,7 @@ def worker_fn() -> RetT:
299
323
return ret
300
324
finally :
301
325
del PARENT_TASK_DATA .token
326
+ del PARENT_TASK_DATA .abandon_on_cancel
302
327
del PARENT_TASK_DATA .cancel_register
303
328
del PARENT_TASK_DATA .task_register
304
329
@@ -336,11 +361,11 @@ def abort(raise_cancel: RaiseCancelT) -> trio.lowlevel.Abort:
336
361
337
362
while True :
338
363
# wait_task_rescheduled return value cannot be typed
339
- msg_from_thread : ThreadDone [RetT ] | Run [object ] | RunSync [
364
+ msg_from_thread : outcome . Outcome [RetT ] | Run [object ] | RunSync [
340
365
object
341
366
] = await trio .lowlevel .wait_task_rescheduled (abort )
342
- if isinstance (msg_from_thread , ThreadDone ):
343
- return msg_from_thread .result . unwrap () # type: ignore[no-any-return]
367
+ if isinstance (msg_from_thread , outcome . Outcome ):
368
+ return msg_from_thread .unwrap () # type: ignore[no-any-return]
344
369
elif isinstance (msg_from_thread , Run ):
345
370
await msg_from_thread .run ()
346
371
elif isinstance (msg_from_thread , RunSync ):
@@ -354,10 +379,10 @@ def abort(raise_cancel: RaiseCancelT) -> trio.lowlevel.Abort:
354
379
355
380
356
381
def from_thread_check_cancelled () -> None :
357
- """Raise trio.Cancelled if the associated Trio task entered a cancelled status.
382
+ """Raise ` trio.Cancelled` if the associated Trio task entered a cancelled status.
358
383
359
384
Only applicable to threads spawned by `trio.to_thread.run_sync`. Poll to allow
360
- ``cancellable=False`` threads to raise :exc:`trio.Cancelled` at a suitable
385
+ ``cancellable=False`` threads to raise :exc:`~ trio.Cancelled` at a suitable
361
386
place, or to end abandoned ``cancellable=True`` threads sooner than they may
362
387
otherwise.
363
388
@@ -366,6 +391,13 @@ def from_thread_check_cancelled() -> None:
366
391
delivery of cancellation attempted against it, regardless of the value of
367
392
``cancellable`` supplied as an argument to it.
368
393
RuntimeError: If this thread is not spawned from `trio.to_thread.run_sync`.
394
+
395
+ .. note::
396
+
397
+ The check for cancellation attempts of ``cancellable=False`` threads is
398
+ interrupted while executing ``from_thread.run*`` functions, which can lead to
399
+ edge cases where this function may raise or not depending on the timing of
400
+ :class:`~trio.CancelScope` shields being raised or lowered in the Trio threads.
369
401
"""
370
402
try :
371
403
raise_cancel = PARENT_TASK_DATA .cancel_register [0 ]
@@ -406,49 +438,6 @@ def _check_token(trio_token: TrioToken | None) -> TrioToken:
406
438
return trio_token
407
439
408
440
409
- def _send_message_to_host_task (
410
- message : Run [RetT ] | RunSync [RetT ], trio_token : TrioToken
411
- ) -> None :
412
- task_register = PARENT_TASK_DATA .task_register
413
-
414
- def in_trio_thread () -> None :
415
- task = task_register [0 ]
416
- if task is None :
417
- # Our parent task is gone! Punt to a system task.
418
- if isinstance (message , Run ):
419
- message .scope .cancel ()
420
- _send_message_to_system_task (message , trio_token )
421
- else :
422
- trio .lowlevel .reschedule (task , outcome .Value (message ))
423
-
424
- trio_token .run_sync_soon (in_trio_thread )
425
-
426
-
427
- def _send_message_to_system_task (
428
- message : Run [RetT ] | RunSync [RetT ], trio_token : TrioToken
429
- ) -> None :
430
- if type (message ) is RunSync :
431
- run_sync = message .run_sync
432
- elif type (message ) is Run :
433
-
434
- def run_sync () -> None :
435
- try :
436
- trio .lowlevel .spawn_system_task (
437
- message .run_system , name = message .afn , context = message .context
438
- )
439
- except RuntimeError : # system nursery is closed
440
- message .queue .put_nowait (
441
- outcome .Error (trio .RunFinishedError ("system nursery is closed" ))
442
- )
443
-
444
- else : # pragma: no cover, internal debugging guard TODO: use assert_never
445
- raise TypeError (
446
- "trio.to_thread.run_sync received unrecognized thread message {!r}."
447
- "" .format (message )
448
- )
449
- trio_token .run_sync_soon (run_sync )
450
-
451
-
452
441
def from_thread_run (
453
442
afn : Callable [..., Awaitable [RetT ]],
454
443
* args : object ,
@@ -467,17 +456,15 @@ def from_thread_run(
467
456
RunFinishedError: if the corresponding call to :func:`trio.run` has
468
457
already completed, or if the run has started its final cleanup phase
469
458
and can no longer spawn new system tasks.
470
- Cancelled: if the corresponding `trio.to_thread.run_sync` task is
471
- cancellable and exits before this function is called, or
472
- if the task enters cancelled status or call to :func:`trio.run` completes
473
- while ``afn(*args)`` is running, then ``afn`` is likely to raise
459
+ Cancelled: if the task enters cancelled status or call to :func:`trio.run`
460
+ completes while ``afn(*args)`` is running, then ``afn`` is likely to raise
474
461
:exc:`trio.Cancelled`.
475
462
RuntimeError: if you try calling this from inside the Trio thread,
476
463
which would otherwise cause a deadlock, or if no ``trio_token`` was
477
464
provided, and we can't infer one from context.
478
465
TypeError: if ``afn`` is not an asynchronous function.
479
466
480
- **Locating a Trio Token **: There are two ways to specify which
467
+ **Locating a TrioToken **: There are two ways to specify which
481
468
`trio.run` loop to reenter:
482
469
483
470
- Spawn this thread from `trio.to_thread.run_sync`. Trio will
@@ -486,17 +473,20 @@ def from_thread_run(
486
473
- Pass a keyword argument, ``trio_token`` specifying a specific
487
474
`trio.run` loop to re-enter. This is useful in case you have a
488
475
"foreign" thread, spawned using some other framework, and still want
489
- to enter Trio, or if you want to avoid the cancellation context of
490
- `trio.to_thread.run_sync`.
476
+ to enter Trio, or if you want to use a new system task to call ``afn``,
477
+ maybe to avoid the cancellation context of a corresponding
478
+ `trio.to_thread.run_sync` task.
491
479
"""
492
- if trio_token is None :
493
- send_message = _send_message_to_host_task
494
- else :
495
- send_message = _send_message_to_system_task
480
+ token_provided = trio_token is not None
481
+ trio_token = _check_token (trio_token )
496
482
497
483
message_to_trio = Run (afn , args , contextvars .copy_context ())
498
484
499
- send_message (message_to_trio , _check_token (trio_token ))
485
+ if token_provided or PARENT_TASK_DATA .abandon_on_cancel :
486
+ message_to_trio .run_in_system_nursery (trio_token )
487
+ else :
488
+ message_to_trio .run_in_host_task (trio_token )
489
+
500
490
return message_to_trio .queue .get ().unwrap () # type: ignore[no-any-return]
501
491
502
492
@@ -522,7 +512,7 @@ def from_thread_run_sync(
522
512
provided, and we can't infer one from context.
523
513
TypeError: if ``fn`` is an async function.
524
514
525
- **Locating a Trio Token **: There are two ways to specify which
515
+ **Locating a TrioToken **: There are two ways to specify which
526
516
`trio.run` loop to reenter:
527
517
528
518
- Spawn this thread from `trio.to_thread.run_sync`. Trio will
@@ -531,15 +521,18 @@ def from_thread_run_sync(
531
521
- Pass a keyword argument, ``trio_token`` specifying a specific
532
522
`trio.run` loop to re-enter. This is useful in case you have a
533
523
"foreign" thread, spawned using some other framework, and still want
534
- to enter Trio, or if you want to avoid the cancellation context of
535
- `trio.to_thread.run_sync`.
524
+ to enter Trio, or if you want to use a new system task to call ``fn``,
525
+ maybe to avoid the cancellation context of a corresponding
526
+ `trio.to_thread.run_sync` task.
536
527
"""
537
- if trio_token is None :
538
- send_message = _send_message_to_host_task
539
- else :
540
- send_message = _send_message_to_system_task
528
+ token_provided = trio_token is not None
529
+ trio_token = _check_token (trio_token )
541
530
542
531
message_to_trio = RunSync (fn , args , contextvars .copy_context ())
543
532
544
- send_message (message_to_trio , _check_token (trio_token ))
533
+ if token_provided or PARENT_TASK_DATA .abandon_on_cancel :
534
+ message_to_trio .run_in_system_nursery (trio_token )
535
+ else :
536
+ message_to_trio .run_in_host_task (trio_token )
537
+
545
538
return message_to_trio .queue .get ().unwrap () # type: ignore[no-any-return]
0 commit comments