1
0
mirror of https://github.com/irmen/prog8.git synced 2025-04-05 18:38:05 +00:00

coroutines: make yield() return a configured uword so that a task subroutine can get reused for multiple different things

This commit is contained in:
Irmen de Jong 2024-12-26 18:55:32 +01:00
parent 4daa909f32
commit f50899c6fa
4 changed files with 73 additions and 34 deletions
compiler/res/prog8lib
docs/source
examples

@ -8,6 +8,8 @@
; - can have a dynamic number of tasks (max 64), when tasks end they're automaticall removed from the task list.
; - you can add new tasks, even from IRQ handlers, while the rest is already running.
; - tasks are regular subroutines but have to call yield() to pass control to the next task (round-robin)
; - yield() returns the registered userdata value for that task, so a single subroutine could be used as multiple tasks on different userdata
; BUT!! in that case, the subroutine cannot have any variables of its own that keep state, because they're shared across the multiple tasks
; - you can kill a task (if you know it's id...)
; - when all tasks are finished the run() call will also return.
; - tasks can't push anything on the cpu stack before calling yield() - that will cause chaos.
@ -17,26 +19,25 @@
; - it's not tied to any IRQ setup, and will run as fast as the tasks themselves allow
; - tasks fully control the switch to the next task; there is no preemptive switching
;
; TODO to make it actually even more useful, we probably have to:
; - return a unique value (pointer that you had to provide when adding the task to the list?)
; from yield() that the subroutine could use to access unique state,
; because right now a single task == a single subroutine; right now you cannot re-use a subroutine to run
; the same task multiple times for different things.
;
; USAGE:
; - call add(taskaddress) to add a new task. It returns the task id.
; - call run() to start executing all tasks until none are left.
; - in tasks: call yield() to pass control to the next task.
; - in tasks: call yield() to pass control to the next task. Use the returned userdata value to do different things.
; - in tasks: if you need that userdata value immediately, simply start the task with a yield() call.
; - call current() to get the current task id.
; - call kill(tasknumber) to kill a task by id.
; - call killall() to kill all tasks.
; - IMPORTANT: if you add the same subroutine multiple times, IT CANNOT DEPEND ON ANY LOCAL VARIABLES OR R0-R15 TO KEEP STATE. NOT EVEN REPEAT LOOP COUNTERS.
; Those are all shared in the different tasks! You HAVE to use a mechanism around the userdata value (pointer?) to keep separate state elsewhere!
coroutines {
const ubyte MAX_TASKS = 64
uword[MAX_TASKS] tasklist
uword[MAX_TASKS] userdatas
uword[MAX_TASKS] returnaddresses
ubyte active_task
sub add(uword taskaddress) -> ubyte {
sub add(uword taskaddress, uword userdata) -> ubyte {
; find the next empty slot in the tasklist and stick it there
; returns the task id of the new task, or 255 if there was no space for more tasks. 0 is a valid task id!
; also returns the success in the Carry flag (carry set=success, carry clear = task was not added)
@ -44,6 +45,7 @@ coroutines {
for cx16.r0L in 0 to len(tasklist)-1 {
if tasklist[cx16.r0L] == 0 {
tasklist[cx16.r0L] = taskaddress
userdatas[cx16.r0L] = userdata
returnaddresses[cx16.r0L] = 0
sys.irqsafe_clear_irqd()
sys.set_carry()
@ -76,16 +78,17 @@ coroutines {
}
}
sub yield() {
; store the return address of the yielding task,
sub yield() -> uword {
; Store the return address of the yielding task,
; and continue with the next one instead (round-robin)
; Returns the associated userdata value
uword task_start, task_continue
returnaddresses[active_task] = sys.popw()
resume_with_next_task:
if not next_task() {
void sys.popw() ; remove return to the termination handler
return ; exiting here will now actually return from the start() call back to the calling program :)
return 0 ; exiting here will now actually return from the start() call back to the calling program :)
}
if task_continue==0 {
@ -96,7 +99,8 @@ resume_with_next_task:
sys.push_returnaddress(task_start)
} else
sys.pushw(task_continue)
; returning from yield then continues with the next coroutine
return userdatas[active_task] ; returning from yield then continues with the next coroutine
sub next_task() -> bool {
; search through the task list for the next active task
@ -122,6 +126,10 @@ resume_with_next_task:
returnaddresses[tasknum] = 0
}
sub current() -> ubyte {
return active_task
}
sub termination() {
; a task has terminated. wipe it from the list.
; this is an internal routine

@ -73,7 +73,7 @@ Subroutines
variables declared in their parent subroutine(s).
- Everything in prog8 is publicly accessible from everywhere else (via fully scoped names) - there is no notion of private or public symbol accessibility.
- Because there is no callstack for subroutine arguments, it becomes very easy to manipulate the return address that *does* get pushed on the stack by the cpu.
With only a little bit of code it is possible to implement a simple cooperative multitasking system that runs multiple tasks simultaneously. See the "coroutines" example.
With only a little bit of code it is possible to implement a simple cooperative multitasking system that runs multiple tasks simultaneously. See the "multitasking" example.
Each task is a subroutine and it simply has its state stored in its statically allocated variables so it can resume after a yield, without doing anything special.
Pointers

@ -72,7 +72,6 @@ IR/VM
Libraries
---------
- coroutines: make yield() return a configured uword so that a task subroutine can get reused for multiple different things
- monogfx: flood fill should be able to fill stippled (it could do this in the past? vm version does it?)
- Sorting module gnomesort_uw could be optimized more, rewrite in asm? Shellshort seems consistently faster even if most of the words are already sorted.
- Add split-word array sorting routines to sorting module?

@ -11,18 +11,50 @@ main {
sub start() {
txt.print("cooperative multitasking / coroutines\n\n")
txt.print("here are couple of routines that each\nrun a few loops bouncing a digit around.\n")
txt.print("press any key to add a new counter task.")
coroutines.killall()
void coroutines.add(&task1)
void coroutines.add(&task2)
void coroutines.add(&task3)
void coroutines.add(&task4)
void coroutines.add(&delaytask)
void coroutines.add(&task1, sc:'1')
void coroutines.add(&task2, sc:'2')
void coroutines.add(&task3, sc:'3')
void coroutines.add(&task4, sc:'4')
void coroutines.add(&keyhandler, 0)
void coroutines.add(&delaytask, 0)
coroutines.run()
txt.print("we're all done!\n")
}
sub keyhandler() {
repeat {
ubyte key = cbm.GETIN2()
if key!=0 {
void coroutines.add(&countertask, key)
}
void coroutines.yield()
}
}
ubyte[coroutines.MAX_TASKS] counters = [100] * coroutines.MAX_TASKS
sub countertask() {
repeat {
uword userdata = coroutines.yield() ; yield and obtain our userdata
ubyte tid = coroutines.current() ; what task are we?
txt.plot(15, 10 + tid)
counters[tid]-- ; our counter is in the array, cannot be a local variable (shared state)
if counters[tid] == 0 {
txt.print(" ")
return ; done, exit the task
} else {
txt.chrout(lsb(userdata))
txt.chrout(':')
txt.print_uw(counters[tid])
txt.spc()
}
}
}
sub task1() {
const ubyte x = 5
ubyte y
@ -30,12 +62,12 @@ main {
for y in 10 to 24 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'1')
coroutines.yield()
void coroutines.yield()
}
for y in 24 downto 10 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'1')
coroutines.yield()
void coroutines.yield()
}
}
txt.setchr(x, 10, sc:' ')
@ -45,21 +77,21 @@ main {
const ubyte x = 10
ubyte y
repeat 2 {
for y in 5 to 18 {
for y in 9 to 22 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'2')
coroutines.yield()
void coroutines.yield()
}
for y in 18 downto 5 {
for y in 22 downto 9 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'2')
coroutines.yield()
void coroutines.yield()
}
}
txt.setchr(x, 5, sc:' ')
txt.setchr(x, 9, sc:' ')
; add a new task dynamically
void coroutines.add(&task5)
void coroutines.add(&task5, 0)
}
sub task3() {
@ -69,12 +101,12 @@ main {
for x in 14 to 38 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'3')
coroutines.yield()
void coroutines.yield()
}
for x in 38 downto 14 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'3')
coroutines.yield()
void coroutines.yield()
}
}
txt.setchr(14, y, sc:' ')
@ -87,12 +119,12 @@ main {
for x in 15 to 30 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'4')
coroutines.yield()
void coroutines.yield()
}
for x in 30 downto 15 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'4')
coroutines.yield()
void coroutines.yield()
}
}
txt.setchr(15, y, sc:' ')
@ -105,12 +137,12 @@ main {
for x in 15 to 30 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'5')
coroutines.yield()
void coroutines.yield()
}
for x in 30 downto 15 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'5')
coroutines.yield()
void coroutines.yield()
}
}
txt.setchr(15, y, sc:' ')
@ -120,7 +152,7 @@ main {
repeat 200 {
sys.waitvsync()
sys.waitvsync()
coroutines.yield()
void coroutines.yield()
}
}
}