Multitasker

Introduction

As of VFX Forth for Mac v4.6, 25 May 2012, there has been an overhaul of the multitasker. All attempts have been abandoned to maintain compatibility with the cooperative multitasker used in MPE embedded systems. The effects of all retained words have not changed. The new tasker does not affect most desktop applications, but does allow us to reduce CPU utilisation and hence power consumption. The original code is still present and may be found as Lib/Osx32/MultiOsx32.trad.fth.

The multitasker supplied with VFX Forth is derived from the multitasker provided with the MPE Forth cross compilers, v6.1 onwards. Using a multitasking system can greatly simplify complex tasks by breaking them down into manageable chunks. This chapter leads you through:

The multitasker source code is in the file Lib/Osx32/MultiOsx32.fth. Note that the full version of this file with all switches set except for test code is compiled as part of the second stage build, but is not present by default in the kernel version of VFX Forth.

Configuration

The configuration of the multitasker is controlled by constants that control what facilities are compiled:

0 constant test-code?           \ -- n
The test code will be compiled if this constant is true and has not already been defined.

Initialising the multitasker

The multitasker needs to be initialised before use. At compile time you must define the tasks that your system requires and at run-time, all the tasks must be initialised.

Before use the multitasker must be initialised by the word INIT-MULTI, which initialises the primary task MAIN, and enables the multi-tasker.

To disable the multitasker, use SINGLE.

To enable the multitasker, use MULTI, which starts the scheduler so new tasks can be added.

Writing a task

Tasks are very straightforward to write, but the way tasks are scheduled needs to be understood. This implementation uses the OS X pthreads API, and so tasks are pre-emptively scheduled. This is different from the cooperative scheduler used by embedded systems. Despite this, the word PAUSE which yields a timeslice is retained for compatibility, and PAUSE is where the MPE event handling is incorporated.


: ACTION1     \ -- ; An example task
  TASK0-IO                 \ select the console as the I/O device
  DUP IP-HANDLE !  OP-HANDLE !
  BEGIN                    \ Start an endless loop
    [CHAR] * EMIT          \ Produce a character  )
    1000 MS                \ Wait 1 second
    PAUSE                  \ Needed!
  AGAIN                    \ Go round again
;
TASK TASK1    \ -- tcb ; name task, get space for it

The task name created by TASK is used as the task identifier by all words that control tasks.

Task dependent variables

An area of memory known as the USER area is set aside for each task. This is often called thread local storage. This memory contains user variables which contain task specific data. For example, the current number conversion radix BASE is normally a user variable as it can vary from task to task.

A user variable is defined in the form:


n USER <name>

where n is the nth byte in the user area. The word +USER can be used to add a user variable of a given size:


  <size> +USER <name>

The use of +USER avoids any need to know the offset at which the variable starts.

A user variable is used in the same way as a normal variable. By stating its name, its address is placed on the stack, which can then be fetched using @ and stored by !.

Controlling tasks

Tasks can be controlled in the following ways:

Activating a task

A task is started by activating it. To activate a task, use INITIATE,


' <action> <task> INITIATE

where ' <action> gives the xt of the word to be run and <task> is the task identifier. The task identifier is used to control the task. Tasks defined by TASK <name> return a task identifier when <name> is executed.

Stopping a task

A task may be temporarily suspended. A task may also halt itself. To temporarily stop a task, use HALT. HALT is used in the form:


<task> HALT

where <task> is the task to be stopped. To restart a halted task, use RESTART which is used in the form:


<task> RESTART

where <task> is the task to restart.

To stop the current task (i.e. stop itself) use *\fo{STOP( -- ).

Terminating a task

Terminating a task halts it, performs an optional clean up action, and calls the operating system thread end function. A thread must terminate itself, which leads to some complexities. However, it does give the task an opportunity to release any resources it may have allocated (especially memory) at start up or during its execution. To terminate a task use:


  <task> TERMINATE

Before the operating system thread end function is called, the terminating task will execute its clean up code. The XT of the clean up code is held in the task control block. If no clean up action is required, zero is used.


  ... ['] CleanUp MyTask AtTaskExit ...

If you want a task to be a BEGIN ... UNTIL loop rather than an endless loop, this is perfectly legal, as returning from a thread will call the clean up code and then the ExitThread function. However, you must define an exit code before you return from the task. Note that on entry to the task there will already be a 0 on the stack.


: MyTask        \ 0 -- exitcode
  <initialisation>         \ initialise task resources
  begin                    \ round and round until done
    <actions>  MyDone @
  until
  drop 0                   \ paranoid, return 0 as success
;

Unlike MPE's embedded systems, under OS X you cannot predict how long a task will take to start after INITIATE or shut down after TERMINATE.

Critical sections

Sometimes the multitasker has to be inhibited so that other tasks are not run during critical operations that would otherwise cause the scheduler to operate. This is achieved using the words SINGLE and MULTI. Note that these do *\{not} stop the OS X scheduler, only the MPE extensions. If a full critical section is required, see the semaphore source to find out how to use the critical section API.


SINGLE   -- ; inhibit tasker
MULTI    -- ; restart tasker

A SEMAPHORE is a structure used for signalling between tasks, and for controlling resource usage. It has two fields, a counter (cell) and an owner (taskid, cell). The counter field is used as a count of the number of times the resource may be used, and the owner field contains the TCB of the task that last gained access. This field can be used for priority arbitration and deadlock detection/arbitration.

This design of a semaphore can be used either to lock a resource such as a comms channel or disc drive during access by one task, or as a counted semaphore controlling access to a buffer. In the second case the counter field contains the number of times the resource can be used. Semaphores are accessed using SIGNAL and REQUEST.

SIGNAL increments the counter field of a semaphore, indicating either that another item has been allocated to the resource, or it is available for use again, 0 indicating that it is in use by a task.

REQUEST waits until the counter field of a semaphore is non-zero, and then decrements the counter field by one. This allows the semaphore to be used as a COUNTED semaphore. For example a character buffer may be used where the semaphore counter shows the number of available characters. Alternatively the semaphore may be used purely to share resources. The semaphore is initialised to one. The first task to REQUEST it gains access, and all other tasks must wait until the accessing task SIGNALs that it has finished with the resource.

Multitasker internals

A multitasker tries to simulate many processors with just one processor. It works by rapidly switching between each task. On each task switch it saves the current state of the processor, and restores the state that the next task needs. The Forth multitasker creates a task control block for each task. The task control block (TCB) is a data structure which contains information relevant to a task.

A simple example

The following example is a simple demonstration of the multitasker. Its role is to display a hash `#' every so often, but leaving the foreground Forth console running. To use the multitasker you must compile the file LIB\MULTIWIN32.FTH into your system. Note that the file has already been compiled by the Studio IDE in VfxForth.exe, but is not present in VfxBase.exe.

The following code defines a simple task called TASK1. It displays a '$' character every second.


VARIABLE DELAY     \ time delay between #'s in milliseconds
  1000 DELAY !             \ initialise time delay
: ACTION1          \ -- ; task to display #'s
  TASK0-IO                 \ select the console as the I/O device
  DUP IP-HANDLE !  OP-HANDLE !
  [CHAR] $ EMIT            \ Display a dollar
  BEGIN                    \ Start continuous loop
    [CHAR] # EMIT          \ Display a hash
    DELAY @ MS             \ Reschedule Delay times
    PAUSE                  \ At least one per loop
  AGAIN                    \ Back to the start ...
;

The use of PAUSE in this example is not actually required as MS periodically calls PAUSE.

Before any tasks can be activated, the multitasker must be initialised. This is done with the following code:


INIT-MULTI

The word INIT-MULTI initialises all the multitasker's data structures and starts multitasking. This word need only be executed once in a multitasking system and is usually executed at start up.

Note that on entry to a task, the stack depth will be 1. This happens because OS X requires a return value when a task terminates, and a value of zero is provided by the task initialisation code.

To run the example task, type:


TASK TASK1
ASSIGN ACTION1 TASK1 INITIATE

This will activate ACTION1 as the action of task TASK1. Immediately you will see a dollar and a hash displayed. If you press <return> a few times, you notice that the Forth interpreter is still running. After a few seconds another hash character will appear. This is the example task working in the background.

The example task can be controlled in several ways:

Changing the variable DELAY can change the rate of production of hashes. Try:


2000 DELAY !

This changes the number of milliseconds between displaying hashes to 2000 milliseconds. Therefore the rate of displaying hashes halves.

Typing the task name followed by HALT halts the task:


TASK1 HALT

You notice that the hashes are not displayed any more.

The task is restarted by RESTART. Type:


TASK1 RESTART

You notice that the hashes are displayed again.

To restart the task from scratch, just kill it and activate it again:


TASK1 TERMINATE
ASSIGN ACTION1 TASK1 INITIATE

You notice the dollar and the hash are displayed, followed by more hashes.

Structures and support

struct /pthread_attr_t  \ -- len
Equivalent of pthread_attr_t structure.

#38 constant /tcb.callback      \ -- len
Size of the task callback data and code. Used for error checks.

struct /TCB     \ -- size
Returns the size of a TCB structure, which controls the task.

  int tcb.link          \ link to next task ; MUST BE FIRST      0
  int tcb.hthread       \ task handle                            4
  int tcb.up            \ user pointer                           8
  int tcb.pumpxt        \ user hook                             12
  int tcb.status        \ status bits                           16
  int tcb.clean         \ xt of clean up handler                20
  /tcb.callback field tcb.callback \ task callback structure    24
  aligned               \ force to cell boundary
end-struct

The task status cell reserves the low 8 bits for use by VFX Forth. The other bits may be used by your application.


Bit    When set                       When Reset
0      task running                   task halted
1..7   RFU                            RFU
8..31  User defined                   User defined

cell +USER ThreadExit?  \ -- addr
Holds a non-zero value to cause the thread to exit.

cell +USER ThreadTCB    \ -- addr
Holds a pointer to the thread's TCB.

cell +USER ThreadSync   \ -- addr
Holds bit patterns used for intertask synchronisation. See later section.

: AtTaskExit    \ xt tcb -- ; set task exit action
Sets the given task's cleanup action. Use in the form:

  ' <action> <task> AtTaskExit

: perform       \ addr --
Execute contents of addr if non-zero. The non-zero contents of addr are EXECUTEd.

Task definition and access

create main     \ -- addr ; tcb of main task
The task structure for the first task run, usually the console or the main application.

: InitTCB       \ addr --
Initialise a task control block at addr.

: task          \ -- ; -- addr ; define task, returns TCB address
Use in the form TASK <name>, creates a new task data structure called <name> which returns the address of the data structure when executed.

: Self          \ -- tcb|0 ; returns TCB of current task
Returns the task id (TCB) of the current task. If called outside a task, zero is returned.

: his           \ task uservar -- addr
Given a task id and a USER variable, returns the address of that variable in the given task. This word is used to set up USER variables in other tasks. Note that the task must be running.

Task handling primitives

0 value multi?  \ -- flag
Returns true if the tasker is enabled.

: single        \ -- ; disable scheduler
Disables the Forth portions of the scheduler, but does not disable OS X scheduling.

: multi         \ -- ; enable scheduler
Enables the Forth portions of the scheduler, but does not disable OS X scheduling.

defer pause     \ --
PAUSE is the software entry to the pre-emptive scheduler, and should be called regularly by all tasks. The phrase sched_yield drop occurs at the end of the default action (PAUSE). If the task needs more than this and does not use one of the existing message loop words such as IDLE, place the XT of the message pump word in offset TCB.PUMPXT of the Task Control Block and that XT will be called once every time PAUSE is called. Because of the way OS X works, PAUSE also controls task closure. A task that does not call PAUSE cannot be safely terminated except by the task itself, or by a call to the API function kill(). A task that calls PAUSE in a loop without calling any delay mechanism will cause CPU hogging.

: (pause)       \ -- ; the scheduler itself
The action of PAUSE after the multitasker has been compiled. If not called from a task, this is a NOOP. If SINGLE has been set, no action is taken. If MULTI is set, the action is sched_yield until the task status is non-zero.

: restart       \ tcb -- ; mark task TCB as running
If the task has been initiated but is now HALTed or STOPped, it will be restarted.

: halt          \ tcb -- ; mark thread as halted
Stops an INITIATEd task from running until RESTART is used.

: stop          \ -- ; halt oneself
HALTs the current task.

: running?      \ tcb -- u
Returns the task's run status, where non-zero indicates that it is running.

Task management

: to-task       \ xt task -- ; set action of task
Used in the form below to define a task's action:

  assign <action> <task> to-task

: to-pump       \ xt task -- ; set message loop of task
Used in the form below to define rhe action of the message pump:

  assign <action> <task> to-pump

: initiate      \ xt task -- ; start task from scratch
Initialises a task running the given xt. All required O/S resources are allocated by this word.

: terminate     \ task -- ; stop task, and remove from list
Causes the specified task to die. You should not make assumptions as to how long this will take. Unlike the embedded systems implementations, this word is very operating system dependent. The task may still be alive on return from this call.

N.B. Do not use self terminate to cause a task to end. Use the following instead:


: Suicide    \ -- ; terminate current task
  ThreadExit? on  begin pause again
;

... Suicide

: start:        \ task -- ; exits from caller
START: is used inside a colon definition. The code before START: is the task's initialisation, performed by the current task. The code after START: up to the closing ; is the action of the task. For example:


  TASK FOO
  : RUN-FOO
    ...
    FOO START:
    ...
    begin  ... pause  again
  ;

All tasks must run in an endless loop, except for initialisation code. There are exceptions to this, and these are discussed in the section on terminating a task. When RUN-FOO is executed, the code after START: is set up as the action of task FOO and started. RUN-FOO then exits. If you want to perform additional actions after starting the task, you should use IINITIATE to start the task.

: TaskState     \ task -- state
Returns true if the task has started and zero if the thread has finished.

: init-multi    \ -- ; initialisation of multi-tasking
Initialise the Forth multitasker to a state where only the task MAIN is known to be running. INIT-MULTI is added to the cold chain and is also called during compilation of MultiOsx32.fth. This word must be run from MAIN.

: term-multi    \ --
Performed in the exit chain when the program terminates. closes all active tasks except SELF. This allows all task clean-up actions to be performed before the program itself finishes.

: .task         \ task -- ; display task name
Given a task, e.g. as returned by SELF, display its name or address.

: .tasks        \ -- ; display active tasks
Display a list of all the active Forth tasks.

Task synchronisation

$AAAA5555 constant TaskReady    \ -- n
At task initiation, USER variable THREADSYNC is set to zero. Set THREADSYNC to this value to indicate that the task is willing to synchronise with another task.

$5555AAAA constant TaskReadied  \ -- n
A synchronising task sets another task's THREADSYNC to this value to indicate that synchronisation is complete.

: WaitForSync   \ --
Perform the slave synchronisation sequence.

: [Sync         \ task -- task
Used by a master task in the form:

  [Sync ... Sync]

to synchronise and pass data to another task, usually when USER variables must be initialised. The slave task must execute WAITFORSYNC.

: Sync]         \ task --
Used by a master task in the form:

  [Sync ... Sync]

to indicate the end of synchronisation.

Semaphores

struct /semaphore       \ -- len
Structure used for OS X i32 semaphores.

: semaphore     \ -- ; -- addr [child]
A SEMAPHORE is an extended variable used for signalling between tasks and for resource allocation. The counter field is used as a count of the number of times the resource may be used, and the arbiter field contains the TCB of the task that last gained access. This field can be used for priority arbitration and deadlock detection/arbitration.

: InitSem       \ semaphore --
Initialise the semaphore. This must be done before using it.

: ShutSem       \ semaphore --
Delete the critical section associated with the smaphore.

: LockSem       \ semaphore --
Lock the semaphore.

: UnlockSem     \ semaphore --
Unlock the semaphore.

: signal        \ sem -- ; increment counter field of semaphore,
SIGNAL increments the counter field of a semaphore, indicating either that another item has been allocated to the resource, or that it is available for use again, 0 indicating in use by a task.

: request       \ sem -- ; get access to resource, wait if count = 0
REQUEST waits until the counter field of a semaphore is non-zero, and then decrements the counter field by one. This allows the semaphore to be used as a counted semaphore. For example a character buffer may be used where the semaphore counter shows the number of available characters. Alternatively the semaphore may be used purely to share resources. The semaphore is initialised to one. The first task to REQUEST it gains access, and all other tasks must wait until the accessing task SIGNALs that it has finished with the resource.

Exclusive access

The words GET and RELEASE access a resource variable to control access to a resource.

: GET           \ varaddr --
If the resource variable is not available, wait until we have set it.

: RELEASE       \ varaddr --
Release the resource variable if it belongs to this task.