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
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.
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,
SET-TASK to make the new task point to the code following the activate word, and
WAKEs 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
ACTIVATEd 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
[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