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.
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.
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.
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.
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 !
.
Tasks can be controlled in the following ways:
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.
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 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
.
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 SIGNAL
s that it has finished with the
resource.
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.
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.
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 EXECUTE
d.
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.
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 HALT
ed or
STOP
ped, it will be restarted.
: halt \ tcb -- ; mark thread as halted
Stops an INITIATE
d task from running until
RESTART
is used.
: stop \ -- ; halt oneself
HALT
s the current task.
: running? \ tcb -- u
Returns the task's run status, where non-zero indicates
that it is running.
: 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.
$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.
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 SIGNAL
s
that it has finished with the resource.
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.