7. Multitasking and Windows

Windows is an inherently multitasked environment. The actual task management algorithms vary between variants of the basic Win32 model, but the interface from the perspective of a program such as SwiftForth and its applications is the same.

The most important fundamental concept is that Windows is an event-driven environment. An event may be I/O (e.g., receipt of a keystroke or mouse click), receipt of a message from another window or program, or an event generated by the OS itself.

7.1 Basic Concepts

This section discusses the underlying principles behind both Windows multitasking and SwiftForth’s facilities for defining and controlling multiple tasks or threads.

7.1.1 Definitions

Multitasking under Windows works at several levels:

Multiple applications (e.g., Word, Excel, Firefox, SwiftForth) can be launched and running concurrently. Each has its own resources (memory, CPU registers and stacks, etc.). Each is a completely different program; they are not sharing any code, although they might call some of the same DLLs. They might also communicate with one another, but they are still separate programs. An application is frequently called a process.

Multiple threads can be launched from within a process. A thread is a piece of code (often a loop) that the Windows OS can be asked to execute separately. The thread uses the resources of the process that launched (and “owns”) it, meaning it is executing code that resides in the owner’s memory, using its registers, etc. However, the code is running asynchronously to the owning application, with the Windows OS controlling the scheduling according to its priority with respect to others. The owning process launches the thread and sets its priority; it can start and stop it, or kill it.

Multiple windows can be launched by a process (or by one of its threads), as well. These are considered child windows. A child window has a procedure (WindowProc) that it executes, which is in many respects like an interrupt. It must start up, initialize itself, perform whatever the event causing the interrupt demands, and exit. The WindowProc is not permitted to have an ongoing behavior or loop, it runs solely in response to events and messages.

A SwiftForth program can support both multiple threads and multiple windows. A thread is in some respects similar to a background task in other FORTH, Inc. products, in that it is given its own stacks and user variables, and is executing code from within the SwiftForth process’s dictionary. However, since the Windows OS controls execution and task switching, there is no equivalent of the round-robin loop found in pF/x or SwiftOS.

Just as you can launch multiple instances of Windows applications, such as Word or Excel, you can also launch multiple instances of SwiftForth. These function as completely independent programs, each of which might have its own threads and/or windows. It is relatively easy for one SwiftForth program to send messages to another.

7.1.2 Forth Reentrancy and Multitasking

When more than one task can share a piece of code, that code can be called reentrant. Reentrancy is valuable in a multitasking system, because it facilitates shared code and simplifies inter-task communication.

Routines that are not reentrant are those containing elements subject to change while the program runs. Thus, self-modifying code is not reentrant. Routines that use global variables are not reentrant.

Forth routines can be made completely reentrant with very little effort. Most keep their intermediate results on the data stack or the return stack. Programs to handle strings or arrays can be designed to keep their data in the section of memory allotted to each task. It is possible to define public routines to access variables, and still retain reentrancy, by providing private versions of these variables to each task; such variables are called user variables.

Since reentrancy is easily achieved, tasks may share routines in a single program space. In SwiftForth, the entire dictionary is shared among tasks.

7.2 SwiftForth Tasks

A task is a Windows thread with additional facilities assigned by SwiftForth. It may be thought of as an entity capable of independently executing Forth definitions. It may be given permanent or temporary job assignments. If it will be given a permanent job assignment, the recommended naming convention is a job title. For example, a task that will acquire data from a remote device attached to a serial port might be called MONITOR.

A task has a separate stack frame assigned to it by Windows, containing its data and return stacks and user variables. SwiftForth may read and write a task’s user variables, but cannot modify its stacks.

7.2.1 User Variables

In SwiftForth, tasks can share code for system and application functions, but each task may have different data for certain facilities. The fact that all tasks have private copies of variable data for such shared functions enables them to run concurrently without conflicts. For example, number conversion in one task needs to control its BASE value without affecting that of other tasks.

Such private variables are referred to as user variables. User variables are not shared by tasks; each task has its own set, kept in the task’s user area.

Executing the name of a user variable returns the address of that particular variable within the task that executes it.

Invoking <taskname> @ returns the address of the first user variable (named STATUS) in task-name’s user area.

Some user variables are defined by the system for its use; they may be found in the file SwiftForth\src\kernel\data.f. You may add more in your application, if you need to in order to preserve reentrancy, to provide private copies of the application-specific data a task might need.

User variables are defined by the defining word +USER, which expects on the stack an offset into the user area plus a size (in bytes) of the new user variable being defined. A copy of the offset will be compiled into the definition of the new word, and the size will be added to it and left on the stack for the next use of +USER. Thus, when specifying a series of user variables, all you have to do is start with an initial offset (#USER) and then specify the sizes. When you are finished defining +USER variables, you may save the current offset to facilitate adding more later, i.e., TO #USER.

It is good practice to group user variables as much as possible, because they are difficult to keep track of if they are scattered throughout your source.

When a task executes a user variable, its offset is added to the register containing the address of the currently executing task’s user area. Therefore, all defined user variables are available to all tasks that have a user area large enough to contain them.

A task may need to initialize another task’s user variables, or to read or modify them. The word HIS allows a task to access another task’s user variables. HIS takes two arguments: the address of the task of interest, and the address of the user variable of interest. For example:

2 MONITOR BASE HIS !

will set the user variable BASE of the task named MONITOR to 2 (binary). In this example, HIS takes the address of the executing task’s BASE and subtracts the address of the start of the executing task’s user area from it to get the offset, then adds the offset to the user area address of the desired task, supplied by MONITOR.

The glossary entries below list words used to manage the user variables. The actual user variables are listed in SwiftForth\src\kernel\data.f.

+USER
( u1 u2 — u3 )

Define a user variable at offset u1 in the user area, and increment the offset by the size u2 to give a new offset u3.

#USER
( — u )

A VALUE that returns the number of bytes currently allocated in a user area. This is the offset for the next user variable when this word is used to start a sequence of +USER definitions intended to add to previously defined user variables.

HIS
( addr1 addr2 — addr3 )

Given a task address addr1 and user variable addr2, returns the address of the referenced user variable in that task’s user area.

7.2.2 Sharing Resources

Some system resources must be shared by tasks without giving any single task permanent control of them. Devices, non-reentrant routines, and shared data areas are all examples of resources available to any task but limited to use by only one task at a time. SwiftForth provides two levels of control: control of an individual resource, or control of the CPU itself within the SwiftForth process.

7.2.2.1 Facility variables

SwiftForth controls access to these resources with two words that resemble Dijkstra’s semaphore operations. (Dijkstra, E.W., Comm. ACM, 18, 9, 569.) These words are GET and RELEASE.

As an example of their use, consider an A/D multiplexer. Various tasks in the system are monitoring certain channels. But it is important that while a conversion is in process, no other task issue a conflicting request. So you might define:

VARIABLE MUX

: A/D ( ch# -- n )   \ Read a value from channel ch#
   MUX GET  (A/D)  MUX RELEASE ;

In the example above, the word A/D requires exclusive use of the multiplexer while it obtains a value using the lower-level word (A/D). The phrase MUX GET obtains private access to this resource. The phrase MUX RELEASE releases it.

In the example above, MUX is an example of a facility variable. When it contains zero, no task is using the facility it represents. When a facility is in use, its facility variable contains the address of the STATUS of the task that owns the facility. The word GET waits until the facility is free or is owned by the task that is running GET.

GET checks a facility repeatedly until it is available. RELEASE checks to see whether a facility is free or is already owned by the task that is executing RELEASE. If it is owned by the current task, RELEASE stores a zero into the facility variable. If it is owned by another task, RELEASE does nothing.

GET and RELEASE can be used safely by any task at any time, as they don’t let any task take a facility from another.

GET
( addr — )

Obtain control of the facility variable at addr. If the facility is owned by another task, the task executing GET will wait until the facility is available.

RELEASE
( addr — )

Relinquish the facility variable at addr. If the task executing RELEASE did not previously own the facility, this operation does nothing.

7.2.2.2 Critical sections

Occasionally, it is necessary to perform a sequence of operations that cannot be interrupted by other SwiftForth tasks. Such a sequence is a critical section, and there are Windows functions to ensure that a critical section can be performed without interruption. SwiftForth’s API to this is in the form of a pair of words, [C and C], which begin and end a critical section. No other SwiftForth task will be permitted to run during the execution of whatever functions lie between these words.

[C
( — )

Begin a critical section. Other SwiftForth tasks cannot execute during a critical section.

C]
( — )

Terminate a critical section.

7.2.2.3 Avoiding deadlocks

SwiftForth does not have any safeguards against deadlocks, in which two (or more) tasks conflict because each wants a resource the other has. For example:

: 1HANG   MUX GET  TAPE GET ... ;
: 2HANG   TAPE GET  MUX GET ... ;

If 1HANG and 2HANG are run by different tasks, they could eventually deadlock.

The best way to avoid deadlocks is to get facilities one at a time, if possible. If you have to get two resources at the same time, it is safest to always request them in the same order. In the multiplexer/tape case, the programmer could use A/D to obtain one or more values stored in a buffer, then move them to tape. In almost all cases, there is a simple way to avoid concurrent GET operations. However, in a poorly written application the conflicting requests might occur on different nesting levels, hiding the problem until a conflict occurs.

It is better to design an application to GET only one resource at a time to avoid deadlocks.

7.2.3 Task Definition and Control

There are two phases to task management: definition and instantiation.

When a task is defined, it gets a dictionary entry containing a Task Control Block, or TCB, which is the table containing its size and other parameters. This happens when a SwiftForth program is compiled, and the task’s definition and TCB are permanent parts of the SwiftForth dictionary.

When a task is instantiated, Windows is requested to allocate a private stack frame to it, within which SwiftForth sets up its data and return stacks and user variables. At this time, the task is also assigned its behavior, or words to execute.

After SwiftForth has instantiated a task, it may communicate with it via the shared memory that is visible to both SwiftForth and the task, or via the task’s user variables.

A task is defined using the sequence:

<size> TASK <taskname>

where size is the requested size of its user area and data stack, combined. The minimum value for size is 4,096 bytes. The task’s return stack is allocated and assigned by Windows. When invoked, taskname will return the address of the task’s TCB.

Task instantiation must be done inside a colon definition, using the form:

: <name>   <taskname> ACTIVATE <words to execute> ;

When name is executed, the task taskname will be instantiated and will begin executing the words that follow ACTIVATE.

The task’s assigned behavior, represented by words to execute above, may be one of two types:

  • Transitory behavior, which the task simply executes and then terminates.
  • Persistent behavior, represented by an infinite (e.g., BEGIN … AGAIN) loop which the task will perform forever (or until reactivated, killed or halted by another task).

Transitory behavior may be terminated by calling the word TERMINATE or simply by returning, in which case SwiftForth will automatically terminate it. A task that has terminated in this fashion may be activated again, to perform the same or a different transitory behavior.

Persistent behavior must include the infinite loop and, within that loop, provision must be made for the task to relinquish the CPU using PAUSE or STOP, or a word that calls one of these (such as MS). These words are discussed in the glossary below. If this is not done, the task cannot be halted or killed and the process that owns it will not terminate properly.

A task that ACTIVATEs another task is that task’s owner. A task may SUSPEND another task; RESUME it, if it has been SUSPENDed; or KILL (uninstantiate) it. A task that is SUSPENDed will always RESUME at the point at which it was SUSPENDed.

A task may also HALT another task, which causes it to cease operation permanently the next time it executes STOP or PAUSE, but leaves it instantiated. The operational distinction between HALT and KILL is that the task remains instantiated after HALT, when it is ACTIVATEd again it will remember any settings in its user variables not directly affected by the task control words.

TASK <taskname>
( u — )

Define a task, whose dictionary size will be u bytes in size. Invoking taskname returns the address of its Task Control Block (TCB).

CONSTRUCT
( addr — )

Instantiate the user area and dictionary of the task whose TCB is at addr and leave a pointer to the task’s memory in the first cell of the TCB. If the task has already been instantiated (i.e. the first cell of the TCB is not zero), CONSTRUCT does nothing. The use of CONSTRUCT is optional; ACTIVATE will do a CONSTRUCT automatically if needed.

ACTIVATE
( addr — )

Instantiate the task whose TCB is at addr (if not already done by CONSTRUCT), and start it executing the words following ACTIVATE. Must be used inside a colon definition.

TERMINATE
( — )

Causes the task executing this word to cease operation and release its memory. A task that terminates itself may be reactivated.

SUSPEND
( addr — )

Force the task whose TCB is at addr to suspend operation indefinitely.

Note: This function is primarily designed for debug use. It is not intended to be used for thread synchronization. Calling SUSPEND on a task that owns a synchronization object, such as a mutex or critical section, can lead to a deadlock if the calling task tries to obtain a synchronization object owned by a suspended task. To avoid this situation, a task within an application that is not a debugger should signal the other task to suspend itself. The target task must be designed to watch for this signal and respond appropriately.

RESUME
( addr — )

Cause the task whose TCB is at addr to resume operation at the point at which it was SUSPENDed (or where the task called STOP).

HALT
( addr — )

Cause the task whose TCB is at addr to cease operation permanently at the next STOP or PAUSE, but to remain instantiated. The task may be reactivated.

KILL
( addr — )

Cause the task whose TCB is at addr to cease operation and release all its memory. The task may be reactivated.

PAUSE
( — )

Relinquish the CPU while checking for messages (if the task has a message queue).

STOP
( — )

Check for messages (if the task has a message queue) and suspend operation indefinitely (until restarted by another task, either with RESUME or ACTIVATE).

Sleep
( u — ior )

Relinquish the CPU for approximately u milliseconds. If u is zero, the task relinquishes the rest of its time slice. Sleep is a Windows call used by MS and PAUSE, and is appropriate when the task wishes to avoid checking its message queue.