Original article appeared in Fourth Dimensions Volume V, Issue 5
Other articles in this series: Laxen multi-tasking one
Multi-Tasking, Part II
Henry Laxen; Berkeley, California
Last time, we saw how to implement the low-level portion of a multi-tasker. We learned that, in Forth, tasks must cooperate with each other and give up control of the CPU at various points. We saw how the PAUSE
and RESTART
words work and how they very efficiently save the status of a task and restore it. This time, we will take a look at how to create tasks and, once started, how to manage them.
Just for the record, let me restate that tasks are linked together in a circular list via the LINK
user variable. A task is active if the ENTRY
user variable contains an RST instruction, and is inactive if it contains a JMP instruction.
The human (I want to say user but will refrain) interface to this mechanism is displayed in figure one. Let’s take a look at what each word does and how it works. First, LOCAL
is a tool that allows one to access a user variable within a specified task. It just computes the actual address of a user variable, given the starting address of the required task. SLEEP
installs a NOP instruction into byte zero of the ENTRY
user variable. Since byte one contains a JMP instruction, the effect of SLEEP
is to guarantee that the next task will get control immediately without this task doing anything. Notice that there is only one instruction (a JMP) executed for each inactive task. This is extremely low overhead. The WAKE
word is he inverse of SLEEP
. It installs an RST instruction into byte zero of ENTRY
. This will eventually cause the RESTART
word to be executed, and awaken this task. Finally, the STOP
word simply puts the current task to sleep and passes control to the next task. WAKE
and SLEEP
both require an argument, which is a pointer to the task that they are to act on, while STOP
acts on the current task and, hence, requires no argument. The names for these functions are extremely apt and I wish the credit for them was mine; but I am afraid they belong to Charles Moore. Thank you, Chuck.
[Figure One] : LOCAL (S base addr -- addr' ) UP @ - + ; : SLEEP (S addr -- ) 0 ( NOP ) SWAP ENTRY LOCAL C! ; : WAKE (S addr -- ) 207 ( RST ) SWAP ENTRY LOCAL C! ; : STOP (S -- ) UP @ SLEEP PAUSE ;
Now that we know how to start a and stop tasks once they exist, let’s take a look at what must be done to set up a task in the first place. The code associated with this appears in figure two. The TASK:
word sets up a task of a specified size. The SET-TASK
word initializes a task so that it is ready to run and the ACTIVATE
word allows you to associate a high-level definition with the task. Let’s look at each word in more detail.
[Figure Two] : TASK: (S size -- ) CREATE TOS HERE #USER @ CMOVE ( Copy the User Area ) HERE ENTRY LOCAL LINK ! ( I point to him ) ENTRY UP @ -ROT HERE UP ! LINK ! ( He points to me ) DUP HERE + DUP RP0 ! 100 - SP0 ! SWAP UP ! ( Reserve space for return stack ) HERE #USER @ + HERE DP LOCAL ! HERE SLEEP ALLOT ;
: SET-TASK (S ip task -- ) DUP SP0 LOCAL @ ( top of stack ) 2- ROT OVER ! ( Initial IP ) 2- OVER RP0 LOCAL @ OVER ! ( Initial RP ) SWAP TOS LOCAL ! ;
: ACTIVATE (S task -- ) R> OVER SET-TASK WAKE ;
Tasks are allocated as part of the dictionary. Also, each task must have its own user area, return stack, parameter stack and dictionary space. This setup is handled in TASK:
which is a defining word that creates a task with a given name and of a specified size. When the name of the task is executed, it returns a pointer to itself. A simple CREATE
suffices for this function, since the word it defines returns its parameter field address.
Next, a copy of the current task’s USER
area is copied to the new task. On line two we set up the current task’s LINK
pointer to point to the new task, and on line three we make the new task point to the old entry point of the current task. We also save a pointer to the current task on the stack. On line five we set up the size of the return stack and the empty parameter stack of the new task, and restore the user pointer to point to the current task. On line size we initialize the new task’s dictionary pointer and, finally, on line seven we put the new task to sleep and allocate space for it in the dictionary of the current task.
SET-TASK
sets up a task for its first execution. It place the initial values of the IP and the return stack pointer onto the new task’s parameter stack, and stuffs the new task’s initial parameter stack value into the TOS
user variable for the new task. In essence, SET-TASK
behaves as though the new task has just done a PAUSE
, and is ready to do a RESTART
. This is what you would expect. Finally, ACTIVATE
uses SET-TASK
to make the new task point to the code following the activate word, and WAKE
s up the new task.
Last but not least, let’s see how we actually set up another task. Figure three illustrates this. On the first line we define a COUNTING
task and allocate 400 bytes for its use. On the next line we simply define a variable called #TIMES
. which will hold the number of times we have counted. Then we define a word called COUNTER
which specifies that the COUNTING
task is to be ACTIVATE
d by explicit use of the PAUSE
word. This is absolutely vital, since this task performs no I/O, hence it must explicitly give up control of the CPU at specified moments. To start running the task, simply execute the word COUNTER
. Now you can watch the behavior of the task by periodically displaying the contents of the variable #TIMES
. You will be able to see it incrementing very rapidly. If you want to stop the new task from executing, you need only type COUNTING SLEEP
. Again, you can query the value of #TIMES
and, indeed, verify that the task has suspended operation. To start it up again, just type COUNTING WAKE
and you will once again be able to see the variable #TIMES
incrementing.
[Figure Three] 400 TASK: COUNTING VARIABLE #TIMES : COUNTER COUNTING ACTIVATE BEGIN 1 #TIMES +! PAUSE AGAIN ; COUNTER
This has been an extremely simple example of a background task. Other applications can be far more useful. For example, you can use the multi-tasker as a mechanism for implementing print spooling and windowing, as well as pipes and filters. I hope these two articles on multi-tasking are a starting point for your own experimentation. Until next time, may the Forth be with you.
Copyright © 1983 by Henry Laxen. All rights reserved.
Other articles in this series: Laxen multi-tasking one