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 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 #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