14 Tasks
Syntax
tasks-decl = "tasks" NAME size [ size [ size [ size ] ] ] [ "apart" ] ; (* count, call depth, region bytes, turn limit *)(1) A task is a flow of execution that takes turns with others inside one state. Tasks change only where one gives way on its own (yield, or a host word that waits). So even when two tasks use the same global, no other task comes in between two yields of one task. Tasks are not threads: a state runs on one thread at a time (5.1).
(2) tasks NAME count [depth [region [turns]]] declares a task group of up to count tasks. Each task has its own call depth (depth) and its own region (region bytes); left out, they are the processor’s defaults. turns, when it is written, is the group’s turn limit (14.6). The name of the group is the type of the values that refer to its tasks. Written last, apart makes a fault inside one of the group’s tasks end that task only (14.4 (3a)); apart is a word of the declaration only in that place.
14.1 Making and running tasks
(1) block GROUP spawn makes one task from a block value (10.5) that takes and gives nothing, and gives the task (or fail, err.full when there is no free slot). A new task is stopped and has not run yet.
(2) GROUP run_all runs the tasks of the group in turn until all have ended. A task runs until it yields or ends; then the next one goes on.
14-tasks.pnx
use io
tasks workers 3
word say (s str) s io.println end
word demo ()
do "a1" say yield or_return "a2" say yield or_return "a3" say end workers spawn or_else return end drop
do "b1" say yield or_return "b2" say end workers spawn or_else return end drop
workers run_all rem run every task of the group until all have ended
end
demo
Output
$ pnx 14-tasks.pnx
a1
b1
a2
b2
a3
(3) TASK resume runs that one task up to its next yield or its end, and gives true when it has ended. TASK is_done asks whether it has ended. The slot of an ended task is used again by spawn.
14-resume.pnx
use io
use str
tasks lone 1
word demo ()
do "step 1" io.println yield or_return "step 2" io.println end lone spawn or_else return end to t
0 to turns
loop t resume until turns 1 add to turns end rem resume gives true when the task has ended
cat ("resumed ", turns 1 add, " times, done: ", t is_done) io.println
end
demo
Output
$ pnx 14-resume.pnx
step 1
step 2
resumed 2 times, done: true
(4) Tasks stay in the state. When a run the host started ends, a stopped task is still there and can go on in the next run.
14-full.pnx
use io
tasks pair 2
word demo ()
do yield or_return end pair spawn or_else return end drop
do yield or_return end pair spawn or_else return end drop
do end pair spawn or_else (e) cat ("third spawn: ", e err_text) io.println return end drop
end
demo
Output
$ pnx 14-full.pnx
third spawn: full
14.2 Yielding
(1) yield passes the turn to another task. It can fail (or fail): when the task was cancelled it fails with err.cancelled. In the main run, which is not a task, yield does nothing and succeeds at once.
(2) Tasks share data through globals, queues and lists. A full or empty queue is a reason to yield.
14-queue.pnx
use io
use str
tasks workers 2
queue jobs int 2
list done int 8
word producer ()
1 to i
loop
i 5 gt until
true to ok
i jobs put or_else (e) false to ok end
ok if i 1 add to i else yield or_return end rem a full queue: let the consumer run
end
end
word consumer ()
0 to got
loop
got 5 ge until
jobs take or_else (e) -1 end to v
v 0 lt if yield or_return else v done push or_else (e) end got 1 add to got end
end
end
word demo ()
do producer end workers spawn or_else return end drop
do consumer end workers spawn or_else return end drop
workers run_all
end
demo
done do (v int) v str.to_str io.print " " io.print end each
"" io.println
Output
$ pnx 14-queue.pnx
1 2 3 4 5
(3) Which words may yield (directly or in a word they call) is known at load. wait_take and wait_put (14.3) yield too. Such words cannot be run from inside a host word (clause 19).
14.3 Waiting for a queue
(1) QUEUE wait_take takes the front value of a queue as take does, but while the queue is empty it yields and looks again when its turn comes back. v QUEUE wait_put puts like put, yielding while the queue is full. Both can fail (or fail): a task cancelled while it waits fails with err.cancelled. When the value can be taken or put at once, cancelling is not looked at.
14-queue-wait.pnx
use io
use str
tasks workers 2
queue jobs int 1
list done int 8
word producer ()
1 to i
loop
i 5 gt until
i jobs wait_put or_return rem waits while the queue is full
i 1 add to i
end
-1 jobs wait_put or_return
end
word consumer ()
loop
jobs wait_take or_return to v rem waits while the queue is empty
v 0 lt until
v done push or_else (e) end
end
end
word demo ()
do producer end workers spawn or_else (e) return end drop
do consumer end workers spawn or_else (e) return end drop
workers run_all
end
demo
done do (v int) v str.to_str io.println end each
Output
$ pnx 14-queue-wait.pnx
1
2
3
4
5
(2) A task that waits for a queue does not run again until some queue has changed. When run_all finds that every task left waits for a queue and none waits for the host, nothing can change a queue any more: that is the fault deadlock. Like a fault inside a task of a group without apart (14.4), it ends every task, those of apart groups too. While a task waits for the host, the state waits (PINAX_WAITING) as before; what that task does when it goes on can free the others.
14-deadlock.pnx
use io
tasks workers 2
queue asks int 1
queue answers int 1
word demo ()
do asks wait_take or_return drop 1 answers wait_put or_return end workers spawn or_else (e) return end drop
do answers wait_take or_return drop 1 asks wait_put or_return end workers spawn or_else (e) return end drop
workers run_all rem each waits for the other: nothing can go
end
demo
"not reached" io.println
Fault
$ pnx 14-deadlock.pnx
14-deadlock.pnx:8: deadlock: waiting for a queue that nothing can change
(3) The main run is not a task and has no one to give its turn to. In the main run, wait_take on an empty queue and wait_put on a full one are the fault deadlock at once.
14-deadlock-main.pnx
use io
use str
queue jobs int 2
word demo ()
jobs wait_take or_else (e) -1 end str.to_str io.println rem the main run is not a task: no one could fill the queue
end
demo
Fault
$ pnx 14-deadlock-main.pnx
14-deadlock-main.pnx:5: deadlock: waiting for a queue that nothing can change
(4) Only these two words are watched for a deadlock. Tasks that wait in a loop of their own (loop … yield …), or a task resumed by hand with
endresume, are not; a host that runs such code gives it a budget (19.5).
14.4 Cancelling
(1) TASK cancel tells a task it is cancelled: its next yield, or its wait for a queue (14.3), fails with err.cancelled. The task takes that failure and ends in its own way. Cancelling does not stop a task by force.
14-cancel.pnx
use io
tasks workers 1
word demo ()
do
loop
"working" io.println
yield or_else (e) cat ("stopped: ", e err_text) io.println return end
end
end workers spawn or_else return end to t
t resume drop
t resume drop
t cancel rem the task's next yield fails with cancelled
t resume drop
end
demo
Output
$ pnx 14-cancel.pnx
working
working
stopped: cancelled
Constraints
(2) The block given to spawn is a block value and does not see the locals around it. Values reach a task through globals and queues.
14-spawn-refused.pnx
tasks workers 2
word demo (n int) do n drop end workers spawn or_else return end drop end
Refused
$ pnx 14-spawn-refused.pnx
14-spawn-refused.pnx:2:22: this name is not declared before this place
(2a) The block of spawn takes nothing and gives nothing. A block with parameters is refused.
14-spawn-block.pnx
tasks workers 2
word start () do (n int) n drop end workers spawn or_else return end drop end
Refused
$ pnx 14-spawn-block.pnx
14-spawn-block.pnx:2:45: spawn takes a block that takes and gives nothing, and a set of tasks: do ... end workers spawn
Faults
(3) A fault inside a task of a group without apart ends the whole run and every task. The fault goes to the host like a fault of the main run. The state remains and takes the next run.
14-fault.pnx
use io
tasks workers 2
global zero int
word demo ()
do "task one" io.println yield or_return "task one again" io.println end workers spawn or_else return end drop
do 1 zero div drop end workers spawn or_else return end drop
workers run_all
end
demo
"not reached" io.println
Fault
$ pnx 14-fault.pnx
task one
14-fault.pnx:6: division by zero
(3a) A fault inside a task of a group declared apart ends that task only. Its slot is free again, and whoever resumed it goes on as after the task’s end: run_all takes the next task, resume gives true. What the task stored until then stays. The run goes on; the host learns of the fault through pinax_task_faults (19.10 (3)).
14-apart.pnx
use io
tasks workers 3 16 1024 apart
word work (n int) cat ("100 / ", n, " = ", 100 n div) io.println end
word demo ()
do 4 work end workers spawn or_else (e) return end drop
do 0 work end workers spawn or_else (e) return end drop rem this one divides by zero
do 5 work end workers spawn or_else (e) return end drop
workers run_all
"the others finished" io.println
end
demo
Output
$ pnx 14-apart.pnx
100 / 4 = 25
100 / 5 = 20
the others finished
(4) Two tasks using the same region are not followed by the check of 12.3. Each task has a region of its own, so this does not normally happen.
Diagnostics
| Condition | Message |
|---|---|
a block with parameters or results given to spawn | spawn takes a block that takes and gives nothing, and a set of tasks: do ... end workers spawn |
resume, is_done or cancel on a value that is not a task | this word takes a task |
run_all on a collection that is not a task group (an array, a list) | run_all takes a set of tasks |
a tasks count above 4096 | a set holds at most 4096 tasks |
| a call depth of a task outside 1 to 4096 | the call depth of a task is 1 to 4096 |
| a region of a task above 16 MiB | the region of a task is at most 16 MiB |
tasks in a data source | a data source runs no code and declares no tasks |
task_tag on a value that is not an int | task_tag takes an int |
| a turn limit below 1 | the turn limit of a task is at least 1 |
14.5 Naming a task
(1) n task_tag (int gives) gives the running task a tag: an int the host is told with the task’s fault when its group is declared apart (14.4 (3a), 19.10 (3)). A task’s tag is 0 until it gives one; a later task_tag replaces it. In the main run, which is not a task, the word does nothing. The tag is a part of the task, and an image keeps it.
(2) A task learns what it serves after spawn, from a queue or a global (14.4 (2)), so it names itself then. A host that gives each request a number tells its tasks the number, and the task gives it as its tag.
14-tag.pnx
use io
tasks workers 2 16 1024 apart
queue jobs int 4
word work ()
jobs wait_take or_return to n
n task_tag rem the host is told this tag if the task faults
cat ("job ", n, ": ", 100 n div) io.println
end
word demo ()
4 jobs wait_put or_return 5 jobs wait_put or_return
do work end workers spawn or_else (e) return end drop
do work end workers spawn or_else (e) return end drop
workers run_all
end
demo
Output
$ pnx 14-tag.pnx
job 4: 25
job 5: 20
14.6 Turn limits
(1) A turn of a task lasts from the moment it gets the run (from run_all or resume) until it yields, waits or ends. A group declared with a turn limit (14 (2)) allows each turn of its tasks at most that many instructions, counted as the budget counts them (19.5): the cost of each check at a back branch or a call, and the bytes scanned. The count starts again at every turn.
(2) A task that runs past its turn limit faults with cancelled at that point. It cannot take the fault as a failure, since nothing there is written to take one. In a group declared apart the task ends alone (14.4 (3a)), and the host is told as of any such fault (19.10 (3a)). In a group without apart the fault ends the run, as any fault inside a task does (14.4 (3)). So a task that loops without yielding is stopped without stopping the others.
14-turns.pnx
use io
tasks workers 2 16 1024 5000 apart rem a turn: at most 5000 before the task yields, waits or ends
word stuck () 0 to i loop i 1 add to i end end rem never yields
word steady ()
0 to i
loop i 1 add to i i 100000 eq until i 100 mod 0 eq if yield or_return end end
cat ("steady counted to ", i) io.println
end
word demo ()
do stuck end workers spawn or_else (e) return end drop
do steady end workers spawn or_else (e) return end drop
workers run_all
"the stuck task was stopped; the steady one finished" io.println
end
demo
Output
$ pnx 14-turns.pnx
steady counted to 100000
the stuck task was stopped; the steady one finished
(3) The limit and the budget are counted apart. When the budget runs out inside a turn, the run is suspended and the turn goes on with what was left of it when the host resumes the run. What the budget still held then, less than the cost of the check it ran out at, counts against the turn as used. A task another task resumes runs on the turn of the one that resumed it, and counts against it too.