diff --git a/compiler/res/prog8lib/coroutines.p8 b/compiler/res/prog8lib/coroutines.p8 index b345fd948..03aed6442 100644 --- a/compiler/res/prog8lib/coroutines.p8 +++ b/compiler/res/prog8lib/coroutines.p8 @@ -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 diff --git a/docs/source/comparing.rst b/docs/source/comparing.rst index a3adab926..6340b36c2 100644 --- a/docs/source/comparing.rst +++ b/docs/source/comparing.rst @@ -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 diff --git a/docs/source/todo.rst b/docs/source/todo.rst index dd7e6b441..b5163a8f9 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -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? diff --git a/examples/multitasking.p8 b/examples/multitasking.p8 index dd5d856f0..c8046c7b4 100644 --- a/examples/multitasking.p8 +++ b/examples/multitasking.p8 @@ -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() } } }