@@ -21,6 +21,9 @@ validation rules to facilitate stack-switching.
21
21
1 . [ Suspending continuations] ( #suspending-continuations )
22
22
1 . [ Partial continuation application] ( #partial-continuation-application )
23
23
1 . [ Continuation lifetime] ( #continuation-lifetime )
24
+ 1 . [ Further examples] ( #further-examples )
25
+ 1 . [ Extending the generator] ( #extending-the-generator )
26
+ 1 . [ Canceling tasks] ( #canceling-tasks )
24
27
1 . [ Design considerations] ( #design-considerations )
25
28
1 . [ Asymmetric switching] ( #asymmetric-switching )
26
29
1 . [ Symmetric switching] ( #symmetric-switching )
@@ -78,12 +81,13 @@ design provides a form of *symmetric switching*.
78
81
79
82
We illustrate the proposed stack-switching mechanism using two
80
83
examples: generators and task scheduling. The generators example uses
81
- asymmetric stack-switching and the task scheduling example uses
82
- symmetric stack-switching.
84
+ asymmetric stack-switching. The task scheduling example has two
85
+ variants: the first variant uses asymmetric stack-switching and the
86
+ second variant uses symmetric stack-switching.
83
87
84
88
### Generators
85
89
86
- The first example illustrates a generator-consumer pattern. Execution
90
+ Our first example illustrates a generator-consumer pattern. Execution
87
91
switches back and forth between a generator and a consumer execution
88
92
stack. Whenever execution switches from the generator to the consumer
89
93
the generator also passes a value to the consumer.
@@ -116,20 +120,19 @@ The overall module implementing our example has the following shape.
116
120
``` wat
117
121
(module $generator
118
122
(type $ft (func))
119
- ;; Types of continuations used by the generator:
123
+ ;; Type of continuations used by the generator:
120
124
;; No need for param or result types: No data passed back to the
121
125
;; generator when resuming it, and $generator function has no return
122
126
;; values.
123
127
(type $ct (cont $ft))
124
128
129
+ (func $print (import "spectest" "print_i32") (param i32))
130
+
125
131
;; Tag used to coordinate between generator and consumer: The i32 param
126
- ;; corresponds to the generated values passed; no values passed back from
127
- ;; generator to consumer.
132
+ ;; corresponds to the generated values passed to consumer ; no values passed
133
+ ;; back from generator to consumer.
128
134
(tag $gen (param i32))
129
135
130
-
131
- (func $print (import "spectest" "print_i32") (param i32))
132
-
133
136
;; Simple generator yielding values from 100 down to 1
134
137
(func $generator ...)
135
138
(elem declare func $generator)
@@ -147,16 +150,16 @@ manipulate suspended continuations of type `(ref $ct)`.
147
150
The generator is defined as follows.
148
151
149
152
``` wat
150
- ;; Simple generator yielding values from 100 down to 1
153
+ ;; Simple generator yielding values from 100 down to 1.
151
154
(func $generator
152
155
(local $i i32)
153
156
(local.set $i (i32.const 100))
154
- (loop $l
155
- ;; Suspend execution, pass current value of $i to consumer
157
+ (loop $loop
158
+ ;; Suspend execution, pass current value of $i to consumer.
156
159
(suspend $gen (local.get $i))
157
- ;; Decrement $i and exit loop once $i reaches 0
160
+ ;; Decrement $i and exit loop once $i reaches 0.
158
161
(local.tee $i (i32.sub (local.get $i) (i32.const 1)))
159
- (br_if $l )
162
+ (br_if $loop )
160
163
)
161
164
)
162
165
```
@@ -178,9 +181,9 @@ The consumer is defined as follows.
178
181
179
182
(loop $loop
180
183
(block $on_gen (result i32 (ref $ct))
181
- ;; Resume continuation $c
184
+ ;; Resume continuation $c.
182
185
(resume $ct (on $gen $on_gen) (local.get $c))
183
- ;; $generator returned: no more data
186
+ ;; $generator returned: no more data.
184
187
(return)
185
188
)
186
189
;; Generator suspended, stack now contains [i32 (ref $ct)]
@@ -257,7 +260,7 @@ The full definition of this module can be found
257
260
258
261
### Task scheduling
259
262
260
- The second example demonstrates how to implement task scheduling with
263
+ Our second example demonstrates how to implement task scheduling with
261
264
the stack-switching instructions. Specifically, suppose we want to
262
265
schedule a number of tasks, represented by functions ` $task_0 ` to
263
266
` $task_n ` , to be executed concurrently. Scheduling is cooperative,
@@ -276,12 +279,10 @@ This approach is illustrated by the following skeleton code.
276
279
277
280
``` wat
278
281
(module $scheduler1
279
-
280
282
(type $ft (func))
281
283
;; Continuation type of all tasks
282
284
(type $ct (cont $ft))
283
285
284
-
285
286
;; Tag used to yield execution in one task and resume another one.
286
287
(tag $yield)
287
288
@@ -442,16 +443,16 @@ continuation references. The task that we switched to is now
442
443
responsible for enqueuing the previous continuation (i.e., the one
443
444
received as a payload) in the task list.
444
445
445
- As a minor complication, we need to encode the fact that the
446
- continuation switched to receives the current one as an argument in
447
- the type of the continuations handled by all scheduling logic. This
448
- means the type ` $ct ` must be recursive: a continuation of this type
446
+ As a minor complication, we must encode the fact that the continuation
447
+ switched to receives the current continuation as an argument in the
448
+ type of the continuations handled by all scheduling logic. This means
449
+ that the type ` $ct ` must be recursive: a continuation of this type
449
450
takes a value of type ` (ref null $ct) ` as a parameter. In order to
450
451
give the same type to continuations that have yielded execution (those
451
452
created by ` switch ` ) and those continuations that correspond to
452
453
beginning the execution of a ` $task_i ` function (those created by
453
454
` cont.new ` ), we add a ` (ref null $ct) ` parameter to all of the
454
- ` $task_i ` functions. Finally, observe that the event loop passes a
455
+ ` $task_i ` functions. Finally, observe that the event loop passes a
455
456
null continuation to any continuation it resumes, indicating to the
456
457
resumed continuation that there is no previous continuation to enqueue
457
458
in the task list.
@@ -682,6 +683,180 @@ In order to ensure that continuations are one-shot, `resume`,
682
683
suspended continuation such that any subsequent use of the same
683
684
suspended continuation will result in a trap.
684
685
686
+ ## Further examples
687
+
688
+ We now illustrate the use of tags with result values and the
689
+ instructions ` cont.bind ` and ` resume.throw ` , by adapting and extending
690
+ the examples of [ Section
691
+ 3] ( #introduction-to-continuation-based-stack-switching ) .
692
+
693
+ ### Extending the generator
694
+
695
+ The ` $generator ` function in [ Section 3] ( #generators )
696
+ produces the values 100 down to 1. It uses the tag ` $gen ` , defined as
697
+ ` (tag $gen (param i32)) ` , to send values to the ` $producer ` function.
698
+
699
+ We now adapt the producer to indicate to the generator when to reset
700
+ (i.e., start counting down from 100 again). The producer is adapted to
701
+ pass a boolean flag to the generator when resuming a continuation.
702
+ Correspondingly, the ` $gen ` tag is adapted to include an ` i32 ` result
703
+ type for the flag:
704
+
705
+ ``` wat
706
+ (tag $gen (param i32) (result i32))
707
+ ```
708
+
709
+ In the generator, the instruction ` (suspend $gen) ` now has type `[ i32]
710
+ -> [ i32] `: the parameter type represents the generated value (as in
711
+ the original version of the example) and the result type represents
712
+ the flag obtained back from the producer. We adapt the generator to
713
+ behave as follows, choosing between resetting or decrementing ` $i ` :
714
+
715
+ ``` wat
716
+ (func $generator
717
+ (local $i i32)
718
+ (local.set $i (i32.const 100))
719
+ (loop $loop
720
+ ;; Suspend execution, pass current value of $i to consumer
721
+ (suspend $gen (local.get $i))
722
+ ;; We now have the flag on the stack given to us by the consumer, telling
723
+ ;; us whether to reset the generator or not.
724
+ (if (result i32)
725
+ (then (i32.const 100))
726
+ (else (i32.sub (local.get $i) (i32.const 1)))
727
+ )
728
+ (local.tee $i)
729
+ (br_if $loop)
730
+ )
731
+ )
732
+ ```
733
+
734
+ In the producer, we add logic to select the value of the flag, and
735
+ pass it to the generator continuation on ` resume ` . However, this poses
736
+ a challenge: the continuation created with `(cont.new $ct0 (ref.func
737
+ $generator)))` has the same type as before: a continuation type with
738
+ no parameter or return types. In contrast, the type of the
739
+ continuation received in a handler block for tag ` $gen ` expects an
740
+ ` i32 ` due to the result type we added to ` $gen ` . This means that the
741
+ producer must now manipulate two different continuation types:
742
+
743
+ ``` wat
744
+ (type $ft0 (func))
745
+ (type $ft1 (func (param i32)))
746
+ ;; Types of continuations used by the generator:
747
+ ;; No param or result types for $ct0: $generator function has no
748
+ ;; parameters or return values.
749
+ (type $ct0 (cont $ft0))
750
+ ;; One param of type i32 for $ct1: An i32 is passed back to the
751
+ ;; generator when resuming it, and $generator function has no return
752
+ ;; values.
753
+ (type $ct1 (cont $ft1))
754
+ ```
755
+
756
+ In order to avoid making the producer function unnecessarily
757
+ complicated, we ensure that there is a single local variable that
758
+ contains the next continuation to resume; its type will be `(ref
759
+ $ct0)` . We can then use ` cont.bind` to turn the continuations received
760
+ in the handler block from type ` (ref $ct1) ` into ` (ref $ct0) ` by
761
+ binding the value of the flag to be passed.
762
+
763
+ The overall function is then defined as follows:
764
+
765
+ ``` wat
766
+ (func $consumer (export "consumer")
767
+ ;; The continuation of the generator.
768
+ (local $c0 (ref $ct0))
769
+ ;; For temporarily storing the continuation received in handler.
770
+ (local $c1 (ref $ct1))
771
+ (local $i i32)
772
+ ;; Create continuation executing function $generator.
773
+ ;; Execution only starts when resumed for the first time.
774
+ (local.set $c0 (cont.new $ct0 (ref.func $generator)))
775
+ ;; Just counts how many values we have received so far.
776
+ (local.set $i (i32.const 1))
777
+
778
+ (loop $loop
779
+ (block $on_gen (result i32 (ref $ct1))
780
+ ;; Resume continuation $c0
781
+ (resume $ct0 (on $gen $on_gen) (local.get $c0))
782
+ ;; $generator returned: no more data
783
+ (return)
784
+ )
785
+ ;; Generator suspended, stack now contains [i32 (ref $ct0)]
786
+ ;; Save continuation to resume it in next iteration
787
+ (local.set $c1)
788
+ ;; Stack now contains the i32 value yielded by $generator
789
+ (call $print)
790
+
791
+ ;; Calculate flag to be passed back to generator:
792
+ ;; Reset after the 42nd iteration
793
+ (i32.eq (local.get $i) (i32.const 42))
794
+ ;; Create continuation of type (ref $ct0) by binding flag value.
795
+ (cont.bind $ct1 $ct0 (local.get $c1))
796
+ (local.set $c0)
797
+
798
+ (local.tee $i (i32.add (local.get $i) (i32.const 1)))
799
+ (br_if $loop)
800
+ )
801
+ )
802
+ ```
803
+
804
+ Here, we set the flag for resetting the generator exactly once (after
805
+ it has returned 42 values).
806
+
807
+ The full version of the extended generator example can be found
808
+ [ here] ( examples/generator-extended.wast ) .
809
+
810
+ ### Canceling tasks
811
+
812
+ The task scheduling examples from [ Section 3] ( #task-scheduling ) yield
813
+ either by suspending to the scheduler running in the parent
814
+ (asymmetric variant) or by calling a scheduling function that uses
815
+ ` switch ` (symmetric variant).
816
+
817
+ Suppose we wish to adapt our schedulers to impose a limit on the
818
+ number of tasks that can exist at the same time. A simple way to
819
+ enforce the limit is to cancel the task at the head of the queue
820
+ whenever an attempt is made to add a continuation to a full queue. We
821
+ can implement this behaviour with a small modification to our previous
822
+ schedulers. Instead of adding tasks to be scheduled directly to a
823
+ queue, we call the following function.
824
+
825
+ ``` wat
826
+ (func $schedule_task (param $c (ref null $ct))
827
+ ;; If the task queue is too long, cancel a task in the queue
828
+ (if (i32.ge_s (call $task_queue-count) (global.get $concurrent_task_limit))
829
+ (then
830
+ (block $exc_handler
831
+ (try_table (catch $abort $exc_handler)
832
+ (resume_throw $ct $abort (call $task_dequeue))
833
+ )
834
+ )
835
+ )
836
+ )
837
+ (call $task_enqueue (local.get $c))
838
+ )
839
+ ```
840
+
841
+ The ` $schedule_task ` function checks if the current number of elements
842
+ in the queue has already reached the limit. If so, the function takes
843
+ an existing continuation from the queue and calls ` resume_throw ` on
844
+ it.
845
+
846
+ The ` resume_throw ` instruction is annotated with a newly defined tag,
847
+ ` $abort ` . This tag denotes an exception that will be raised at the
848
+ suspension point of the continuation. We then wrap the ` resume_throw `
849
+ instruction in a ` try_table ` , which installs an exception handler for
850
+ ` $abort ` . This exception handler simply swallows the exception, which
851
+ means that the exception raised at the suspension point cannot escape
852
+ the ` $schedule_task ` function. The old continuation is deallocated and
853
+ the function proceeds to enqueue the new continuation.
854
+
855
+ The full version of the symmetric scheduling example using the
856
+ ` $schedule_task ` function can be found
857
+ [ here] ( examples/scheduler2-throw.wast ) . The changes to the asymmetric
858
+ scheduling example are analogous.
859
+
685
860
## Design considerations
686
861
687
862
In this section we discuss some key design considerations.
0 commit comments