@@ -436,6 +436,16 @@ ability to switch stacks. In any case, the use of `threading.Thread` is
436436encapsulated by the ` Thread ` class so that the rest of the Canonical ABI can
437437simply use ` suspend ` , ` resume ` , etc.
438438
439+ When a ` Thread ` is suspended and then resumed, it receives a ` SuspendResult `
440+ value indicating whether the caller has cooperatively requested that the thread
441+ cancel itself which is communicated to Core WebAssembly with the following
442+ integer values:
443+ ``` python
444+ class SuspendResult (IntEnum ):
445+ NOT_CANCELLED = 0
446+ CANCELLED = 1
447+ ```
448+
439449Introducing the ` Thread ` class in chunks, a ` Thread ` has the following fields
440450and can be in one of the following 3 states based on these fields:
441451* ` running ` : actively executing with a "parent" thread that is waiting
@@ -451,7 +461,7 @@ class Thread:
451461 parent_lock: Optional[threading.Lock]
452462 ready_func: Optional[Callable[[], bool ]]
453463 cancellable: bool
454- cancelled: bool
464+ suspend_result: Optional[SuspendResult]
455465 in_event_loop: bool
456466 index: Optional[int ]
457467 context: list[int ]
@@ -490,13 +500,14 @@ immediately blocked `acquire()`ing `fiber_lock` (which will be `release()`ed by
490500 self .parent_lock = None
491501 self .ready_func = None
492502 self .cancellable = False
493- self .cancelled = False
503+ self .suspend_result = None
494504 self .in_event_loop = False
495505 self .index = None
496506 self .context = [0 ] * Thread.CONTEXT_LENGTH
497507 def fiber_func ():
498508 self .fiber_lock.acquire()
499- assert (self .running())
509+ assert (self .running() and self .suspend_result == SuspendResult.NOT_CANCELLED )
510+ self .suspend_result = None
500511 thread_func(self )
501512 assert (self .running())
502513 self .task.thread_stop(self )
@@ -529,11 +540,10 @@ instruction in the Core WebAssembly [stack-switching] proposal). This waiting
529540is accomplished using the ` parent_lock ` field of the resumed thread, which the
530541resumed thread will ` release() ` when it suspends or exits.
531542
532- One extra boolean value communicated from ` resume ` to ` suspend ` is requests for
533- cancellation. When a thread calls ` Thread.suspend ` , it indicates whether it is
534- able to handle cancellation. This information is stored in the ` cancellable `
535- field which is used by ` Task.request_cancellation ` (defined below) to only
536- ` resume ` with ` cancel = True ` when the thread expects it.
543+ When a thread calls ` Thread.suspend ` , it indicates whether it is able to handle
544+ cancellation. This information is stored in the ` cancellable ` field which is
545+ used by ` Task.request_cancellation ` (defined below) to only ` resume ` with
546+ ` SuspendResult.CANCELLED ` when the thread expects it.
537547
538548Lastly, several ` Thread ` methods below will set the ` ready_func ` and add the
539549` Thread ` to the ` Store.pending ` list so that ` Store.tick ` will call ` resume `
@@ -544,32 +554,33 @@ when the `ready_func` returns `True`. Once `Thread.resume` is called, the
544554Given the above, ` Thread.resume ` and ` Thread.suspend ` can be defined
545555complementarily using ` parent_lock ` and ` fiber_lock ` as follows:
546556``` python
547- def resume (self , cancel = False ):
548- assert (not self .running() and not self .cancelled )
557+ def resume (self , suspend_result = SuspendResult. NOT_CANCELLED ):
558+ assert (not self .running() and self .suspend_result is None )
549559 if self .ready_func:
550- assert (cancel or self .ready_func())
560+ assert (suspend_result == SuspendResult. CANCELLED or self .ready_func())
551561 self .ready_func = None
552562 self .task.inst.store.pending.remove(self )
553- assert (self .cancellable or not cancel )
554- self .cancelled = cancel
563+ assert (self .cancellable or suspend_result == SuspendResult. NOT_CANCELLED )
564+ self .suspend_result = suspend_result
555565 self .parent_lock = threading.Lock()
556566 self .parent_lock.acquire()
557567 self .fiber_lock.release()
558568 self .parent_lock.acquire()
559569 self .parent_lock = None
560570 assert (not self .running())
561571
562- def suspend (self , cancellable ) -> bool :
563- assert (self .running() and not self .cancellable and not self .cancelled )
572+ def suspend (self , cancellable ) -> SuspendResult :
573+ assert (self .running() and not self .cancellable and self .suspend_result is None )
564574 self .cancellable = cancellable
565575 self .parent_lock.release()
566576 self .fiber_lock.acquire()
567577 assert (self .running())
568578 self .cancellable = False
569- completed = not self .cancelled
570- self .cancelled = False
571- assert (cancellable or completed)
572- return completed
579+ suspend_result = self .suspend_result
580+ self .suspend_result = None
581+ assert (suspend_result is not None )
582+ assert (cancellable or suspend_result == SuspendResult.NOT_CANCELLED )
583+ return suspend_result
573584```
574585
575586The ` Thread.resume_later ` method is called by ` canon_thread_resume_later ` below
@@ -586,10 +597,10 @@ in the near future:
586597The ` Thread.suspend_until ` method is used by a multiple internal callers below
587598to specify a custom ` ready_func ` that is polled by ` Store.tick ` :
588599``` python
589- def suspend_until (self , ready_func , cancellable = False ) -> bool :
600+ def suspend_until (self , ready_func , cancellable = False ) -> SuspendResult :
590601 assert (self .running())
591602 if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0 ,1 ):
592- return True
603+ return SuspendResult. NOT_CANCELLED
593604 self .ready_func = ready_func
594605 self .task.inst.store.pending.append(self )
595606 return self .suspend(cancellable)
@@ -610,10 +621,11 @@ of internal `thread.switch-to`s before suspending, the `async`-lowered caller
610621resumes execution immediately (as if there were no ` thread.switch-to ` and
611622[ Asyncify] was used to emulate stack switching instead).
612623``` python
613- def switch_to (self , cancellable , other : Thread) -> bool :
614- assert (self .running() and other.suspended() )
615- assert (not self .cancellable )
624+ def switch_to (self , cancellable , other : Thread) -> SuspendResult :
625+ assert (self .running() and not self .cancellable and self .suspend_result is None )
626+ assert (other.suspended() and other.suspend_result is None )
616627 self .cancellable = cancellable
628+ other.suspend_result = SuspendResult.NOT_CANCELLED
617629 assert (self .parent_lock and not other.parent_lock)
618630 other.parent_lock = self .parent_lock
619631 self .parent_lock = None
@@ -622,16 +634,18 @@ resumes execution immediately (as if there were no `thread.switch-to` and
622634 self .fiber_lock.acquire()
623635 assert (self .running())
624636 self .cancellable = False
625- completed = not self .cancelled
626- self .cancelled = False
627- return completed
637+ suspend_result = self .suspend_result
638+ self .suspend_result = None
639+ assert (suspend_result is not None )
640+ assert (cancellable or suspend_result == SuspendResult.NOT_CANCELLED )
641+ return suspend_result
628642```
629643
630644Lastly, the ` Thread.yield_to ` method is used by ` canon_thread_yield_to ` below
631645to switch execution to some other thread (like ` Thread.switch_to ` ), but leave
632646the current thread ` ready ` instead of ` suspended ` .
633647``` python
634- def yield_to (self , cancellable , other : Thread) -> bool :
648+ def yield_to (self , cancellable , other : Thread) -> SuspendResult :
635649 assert (not self .ready_func)
636650 self .ready_func = lambda : True
637651 self .task.inst.store.pending.append(self )
@@ -889,9 +903,9 @@ backpressure is disabled. There are three sources of backpressure:
889903 return self .inst.backpressure > 0 or (self .needs_exclusive() and self .inst.exclusive)
890904 if has_backpressure() or self .inst.num_waiting_to_enter > 0 :
891905 self .inst.num_waiting_to_enter += 1
892- completed = thread.suspend_until(lambda : not has_backpressure(), cancellable = True )
906+ result = thread.suspend_until(lambda : not has_backpressure(), cancellable = True )
893907 self .inst.num_waiting_to_enter -= 1
894- if not completed :
908+ if result == SuspendResult. CANCELLED :
895909 self .cancel()
896910 return False
897911 if self .needs_exclusive():
@@ -940,7 +954,7 @@ that it can be delivered in the future by `Task.deliver_pending_cancel`.
940954 for thread in self .threads:
941955 if thread.cancellable and not (thread.in_event_loop and self .inst.exclusive):
942956 self .state = Task.State.CANCEL_DELIVERED
943- thread.resume(cancel = True )
957+ thread.resume(SuspendResult. CANCELLED )
944958 return
945959 self .state = Task.State.PENDING_CANCEL
946960
@@ -963,28 +977,28 @@ and `needs_exclusive`).
963977The following ` Task ` methods wrap corresponding ` Thread ` methods after first
964978delivering any pending cancellations set by ` Task.request_cancellation ` :
965979``` python
966- def suspend (self , thread , cancellable ) -> bool :
980+ def suspend (self , thread , cancellable ) -> SuspendResult :
967981 assert (thread in self .threads and thread.task is self )
968982 if self .deliver_pending_cancel(cancellable):
969- return False
983+ return SuspendResult. CANCELLED
970984 return thread.suspend(cancellable)
971985
972- def suspend_until (self , ready_func , thread , cancellable ) -> bool :
986+ def suspend_until (self , ready_func , thread , cancellable ) -> SuspendResult :
973987 assert (thread in self .threads and thread.task is self )
974988 if self .deliver_pending_cancel(cancellable):
975- return False
989+ return SuspendResult. CANCELLED
976990 return thread.suspend_until(ready_func, cancellable)
977991
978- def switch_to (self , thread , cancellable , other_thread ) -> bool :
992+ def switch_to (self , thread , cancellable , other_thread ) -> SuspendResult :
979993 assert (thread in self .threads and thread.task is self )
980994 if self .deliver_pending_cancel(cancellable):
981- return False
995+ return SuspendResult. CANCELLED
982996 return thread.switch_to(cancellable, other_thread)
983997
984- def yield_to (self , thread , cancellable , other_thread ) -> bool :
998+ def yield_to (self , thread , cancellable , other_thread ) -> SuspendResult :
985999 assert (thread in self .threads and thread.task is self )
9861000 if self .deliver_pending_cancel(cancellable):
987- return False
1001+ return SuspendResult. CANCELLED
9881002 return thread.yield_to(cancellable, other_thread)
9891003```
9901004
@@ -1000,10 +1014,11 @@ trap if another task tries to drop the waitable set being used.
10001014 wset.num_waiting += 1
10011015 def ready_and_has_event ():
10021016 return ready_func() and wset.has_pending_event()
1003- if not self .suspend_until(ready_and_has_event, thread, cancellable):
1004- event = (EventCode.TASK_CANCELLED , 0 , 0 )
1005- else :
1006- event = wset.get_pending_event()
1017+ match self .suspend_until(ready_and_has_event, thread, cancellable):
1018+ case SuspendResult.CANCELLED :
1019+ event = (EventCode.TASK_CANCELLED , 0 , 0 )
1020+ case SuspendResult.NOT_CANCELLED :
1021+ event = wset.get_pending_event()
10071022 wset.num_waiting -= 1
10081023 return event
10091024```
@@ -1018,12 +1033,14 @@ nondeterministically switch to another task (or not).
10181033 def poll_until (self , ready_func , thread , wset , cancellable ) -> Optional[EventTuple]:
10191034 assert (thread in self .threads and thread.task is self )
10201035 wset.num_waiting += 1
1021- if not self .suspend_until(ready_func, thread, cancellable):
1022- event = (EventCode.TASK_CANCELLED , 0 , 0 )
1023- elif wset.has_pending_event():
1024- event = wset.get_pending_event()
1025- else :
1026- event = (EventCode.NONE , 0 , 0 )
1036+ match self .suspend_until(ready_func, thread, cancellable):
1037+ case SuspendResult.CANCELLED :
1038+ event = (EventCode.TASK_CANCELLED , 0 , 0 )
1039+ case SuspendResult.NOT_CANCELLED :
1040+ if wset.has_pending_event():
1041+ event = wset.get_pending_event()
1042+ else :
1043+ event = (EventCode.NONE , 0 , 0 )
10271044 wset.num_waiting -= 1
10281045 return event
10291046```
@@ -1034,10 +1051,11 @@ the event loop in `canon_lift` when `CallbackCode.YIELD` is returned.
10341051``` python
10351052 def yield_until (self , ready_func , thread , cancellable ) -> EventTuple:
10361053 assert (thread in self .threads and thread.task is self )
1037- if not self .suspend_until(ready_func, thread, cancellable):
1038- return (EventCode.TASK_CANCELLED , 0 , 0 )
1039- else :
1040- return (EventCode.NONE , 0 , 0 )
1054+ match self .suspend_until(ready_func, thread, cancellable):
1055+ case SuspendResult.CANCELLED :
1056+ return (EventCode.TASK_CANCELLED , 0 , 0 )
1057+ case SuspendResult.NOT_CANCELLED :
1058+ return (EventCode.NONE , 0 , 0 )
10411059```
10421060
10431061The ` Task.return_ ` method is called by ` canon_task_return ` and ` canon_lift ` to
@@ -4386,27 +4404,20 @@ index `$i` from the current component instance's table, traps if it's not
43864404[ suspended] , and then switches to that thread, leaving the [ current thread]
43874405suspended.
43884406``` python
4389- class SuspendResult (IntEnum ):
4390- COMPLETED = 0
4391- CANCELLED = 1
4392-
43934407def canon_thread_switch_to (cancellable , thread , i ):
43944408 trap_if(not thread.task.inst.may_leave)
43954409 other_thread = thread.task.inst.table.get(i)
43964410 trap_if(not isinstance (other_thread, Thread))
43974411 trap_if(not other_thread.suspended())
4398- if not thread.task.switch_to(thread, cancellable, other_thread):
4399- assert (cancellable)
4400- return [SuspendResult.CANCELLED ]
4401- else :
4402- return [SuspendResult.COMPLETED ]
4412+ suspend_result = thread.task.switch_to(thread, cancellable, other_thread)
4413+ return [suspend_result]
44034414```
4404- If ` cancellable ` is set, then ` thread.switch-to ` will return whether the
4405- supertask has already or concurrently requested cancellation. ` thread.switch-to `
4406- (and other cancellable operations) will only indicate cancellation once and
4407- thus, if a caller is not prepared to propagate cancellation, they can omit
4408- ` cancellable ` so that cancellation is instead delivered at a later
4409- ` cancellable ` call.
4415+ If ` cancellable ` is set, then ` thread.switch-to ` will return a ` SuspendResult `
4416+ value to indicate whether the supertask has already or concurrently requested
4417+ cancellation. ` thread.switch-to ` (and other cancellable operations) will only
4418+ indicate cancellation once and thus, if a caller is not prepared to propagate
4419+ cancellation, they can omit ` cancellable ` so that cancellation is instead
4420+ delivered at a later ` cancellable ` call.
44104421
44114422
44124423### 🧵 ` canon thread.suspend `
@@ -4424,18 +4435,15 @@ calling component.
44244435``` python
44254436def canon_thread_suspend (cancellable , thread ):
44264437 trap_if(not thread.task.inst.may_leave)
4427- if not thread.task.suspend(thread, cancellable):
4428- assert (cancellable)
4429- return [SuspendResult.CANCELLED ]
4430- else :
4431- return [SuspendResult.COMPLETED ]
4438+ suspend_result = thread.task.suspend(thread, cancellable)
4439+ return [suspend_result]
44324440```
4433- If ` cancellable ` is set, then ` thread.suspend ` will return whether the
4434- supertask has already or concurrently requested cancellation. ` thread.suspend `
4435- (and other cancellable operations) will only indicate cancellation once and
4436- thus, if a caller is not prepared to propagate cancellation, they can omit
4437- ` cancellable ` so that cancellation is instead delivered at a later
4438- ` cancellable ` call.
4441+ If ` cancellable ` is set, then ` thread.suspend ` will return a ` SuspendResult `
4442+ value to indicate whether the supertask has already or concurrently requested
4443+ cancellation. ` thread.suspend ` (and other cancellable operations) will only
4444+ indicate cancellation once and thus, if a caller is not prepared to propagate
4445+ cancellation, they can omit ` cancellable ` so that cancellation is instead
4446+ delivered at a later ` cancellable ` call.
44394447
44404448
44414449### 🧵 ` canon thread.resume-later `
@@ -4485,18 +4493,15 @@ def canon_thread_yield_to(cancellable, thread, i):
44854493 other_thread = thread.task.inst.table.get(i)
44864494 trap_if(not isinstance (other_thread, Thread))
44874495 trap_if(not other_thread.suspended())
4488- if not thread.task.yield_to(thread, cancellable, other_thread):
4489- assert (cancellable)
4490- return [SuspendResult.CANCELLED ]
4491- else :
4492- return [SuspendResult.COMPLETED ]
4496+ suspend_result = thread.task.yield_to(thread, cancellable, other_thread)
4497+ return [suspend_result]
44934498```
4494- If ` cancellable ` is set, then ` thread.yield-to ` will return whether the
4495- supertask has already or concurrently requested cancellation. ` thread.yield-to `
4496- (and other cancellable operations) will only indicate cancellation once and
4497- thus, if a caller is not prepared to propagate cancellation, they can omit
4498- ` cancellable ` so that cancellation is instead delivered at a later
4499- ` cancellable ` call.
4499+ If ` cancellable ` is set, then ` thread.yield-to ` will return a ` SuspendResult `
4500+ value indicating whether the supertask has already or concurrently requested
4501+ cancellation. ` thread.yield-to ` (and other cancellable operations) will only
4502+ indicate cancellation once and thus, if a caller is not prepared to propagate
4503+ cancellation, they can omit ` cancellable ` so that cancellation is instead
4504+ delivered at a later ` cancellable ` call.
45004505
45014506
45024507### 🧵 ` canon thread.yield `
@@ -4519,19 +4524,20 @@ def canon_thread_yield(cancellable, thread):
45194524 event_code,_,_ = thread.task.yield_until(lambda : True , thread, cancellable)
45204525 match event_code:
45214526 case EventCode.NONE :
4522- return [SuspendResult.COMPLETED ]
4527+ return [SuspendResult.NOT_CANCELLED ]
45234528 case EventCode.TASK_CANCELLED :
45244529 return [SuspendResult.CANCELLED ]
45254530```
45264531Even though ` yield_until ` passes ` lambda: True ` as the condition it is waiting
45274532for, ` yield_until ` does transitively peform a ` Thread.suspend ` which allows
45284533the embedder to nondeterministically switch to executing another thread.
45294534
4530- If ` cancellable ` is set, then ` thread.yield ` will return whether the supertask
4531- has already or concurrently requested cancellation. ` thread.yield ` (and other
4532- cancellable operations) will only indicate cancellation once and thus, if a
4533- caller is not prepared to propagate cancellation, they can omit ` cancellable `
4534- so that cancellation is instead delivered at a later ` cancellable ` call.
4535+ If ` cancellable ` is set, then ` thread.yield ` will return a ` SuspendResult `
4536+ value indicating whether the supertask has already or concurrently requested
4537+ cancellation. ` thread.yield ` (and other cancellable operations) will only
4538+ indicate cancellation once and thus, if a caller is not prepared to propagate
4539+ cancellation, they can omit ` cancellable ` so that cancellation is instead
4540+ delivered at a later ` cancellable ` call.
45354541
45364542
45374543### 📝 ` canon error-context.new `
0 commit comments