7. EXCEPTION HANDLING AND MULTITASKING
7.1 EXCEPTION HANDLING
The following sections provide an overview of the RED language exception
handling facilities. Features provided are described in the context of the
real-time applications they are designed to support.
7.1.1 RAISING EXCEPTIONS
Exceptions may be raised explicitly by the user or implicitly by the system.
System-defined exceptions include: arithmetic overflow, underflow, and zero-divide;
range violations on assignment, case, subscript, and exponent; and use of
uninitialized data. The user may define other exceptions via an exception
declaration. The naming and scoping of exceptions follow the same rules as all
other named objects in the language.
An exception may be explicitly raised by a RAISE statement. When an exception
is raised, control is transferred to an appropriate handler. The handler may, in
turn, raise some other exception. Occasionally, a handler may perform some specific
processing and then pass the exception on to some other handler. For instance, an
I/O capsule may export several exceptions and may enforce some rules about the
maximum number of tolerable errors. Whenever an exception is raised in one of the
capsule's routines, the routine handles the exception by performing the appropriate
bookkeeping and then passing the original exception on to the invoker.
The reraising of an exception is accomplished with the RERAISE statement.
Using the RAISE statement with no exception specified was considered (in the
interest of reducing the number of reserved words) but was rejected on reliability
grounds. when initially sketching out code, a programmer could easily write in the
beginning of a RAISE statement, intending subsequently to fill in the actual
exception name. Omission of such details is a common coding problem and one of the
main motivations for not allowing default declarations. The separate reraise syntax
guarantees that the same exception will be reraised only if the programmer really
intended that to happen.
7.1.2 HANDLING EXCEPTIONS
Once an exception has been raised at run time, the system searches for a
handler. Handlers are found in GUARD statements of the form:
When an exception occurs during the elaboration of the guarded body, control is
transferred to the appropriate member of the WHEN list. The WHEN list and ELSE
clause are optional and the identifiers that specify which exceptions are handled
may, in fact, be lists of identifiers. The syntactic similarity with the CASE and
WAIT statements is intentional, since the three statements perform tasks that are
conceptually similar.
If an exception occurs during execution of a guarded body and an appropriate
handler is supplied in the guard statement, the choice of a handler is
straightforward. In practice, however, this will generally not be the case. In a
structured program, one would expect the guarded bodies to invoke lower level
routines. The lower level routines contain the detailed computations that may raise
exceptions, but lack the global viewpoint to handle the errors. It is therefore
necessary to be able to find a handler when the statement raising the exception is
not itself a constituent of a guarded body.
The requirements for such processing are described in SM 1OC. Within a
routine, a search is made for a handler by working outwards through the statically
nested guarded bodies. If no handler is found, the exception is reraised at the
point of call, thus allowing the exception to be raised in progressively higher-level
routines. If an exception is raised but not handled in a task, that task is
terminated but no other task is affected.
The RED language is in compliance with Steelman 1OC. The search for a handler
begins in the scope in which the exception is raised and progresses according to the
following rules:
- If the scope is a GUARD statement with an appropriate handler, apply the
handler.
- If the scope is a routine, reraise the exception at the point of invocation.
- If the scope is a task, terminate the task.
- If the scope is the body which declares the exception name, raise the
X_UNHANDLED exception in the enclosing scope.
- If none of rules 1-4 apply, reraise the exception in the enclosing scope.
The search for a handler may entail leaving several scopes. Exiting scopes due
to exception processing entails the same storage deallocations as does normal
termination. As a result, RED requires that exit from a scope be deferred until all
subtasks created therein have terminated. If this is not the desired behavior, the
user need simply provide a guard statement with an ELSE clause which exterminates
the subtasks and then reraises the exception.
7.1.3 TERMINATING OTHER TASKS
Although it is dangerous for one task to terminate another, it is occasionally
necessary to take this kind of action. For instance, a computer system controlling
the flight of an airplane might support a variety of functions not required for the
survival of the aircraft and its passengers. Should an emergency occur, all
auxiliary functions would have to be shut down quickly so as to bring all the
available resources to bear on the immediate problem. By permitting such
termination, the RED facility maximizes safety without depriving the user of the
power and flexibility necessary to get the job done.
The procedure call
exterminate(activation_name)
will cause the X_TERMINATE exception to be raised in the named activation.
Exterminate is a procedure in the predefined capsule which defines the scheduler.
Notice that exterminate invokes a capability of the scheduler. This is necessary to
guarantee that things are done in an expeditious but orderly manner. In particular,
regardless of the state of the activation before the exterminate call, the scheduler
will put it in the ready state about to execute a RAISE X_TERMINATE. The RAISE
statement will actually be elaborated whenever the task is next assigned a
processor; this depends upon the task's relative priority. Once the X_TERMINATE
exception has been raised, it is treated like any other exception, the actual
termination of the task being caused by the absence of a handler which does not
perform a reraise.
There are occasionally critical points at which a program should not be
interrupted. These typically occur in connection with manipulating locks on data.
The procedures CRITICAL and NONCRITICAL, respectively, serve to disable and reenable
X_TERMINATE. The semantics of the REGION statement guarantees that the associated
lock will always be unlocked upon exit from the region. Conceptually, this is done
in the presence of exceptions by executing:
Note that if an X_TERMINATE is raised between the lock and the guard, the lock will
never be unlocked. To prevent this, X_TERMINATE must be disabled during the
lock-guard sequence as in
7.2 MULTITASKING
7.2.1 INTRODUCTION
The RED language provides two levels of multitasking primitives. At the higher
level, primitives are available for creating and terminating activations of tasks,
for sending and receiving messages between activations, and for synchronizing the
use of shared data. These primitives make use of a scheduler that provides service
to activations based on priority and time of request for service.
At the lower level, RED provides primitives that can be used to define
different schedulers, and to define alternate methods of message passing and
synchronization. These low-level primitives are sufficiently powerful that the
high-level primitives can be implemented in terms of them. Also, interrupt handling
can be accomplished through the low-level facilities.
We expect that most application programs will use the high-level primitives.
If low-level primitives must be used to implement alternatives to the high-level
facilities, these alternatives can be used in the same manner as the built-in ones
(e.g., via the WAIT and REGION statements). For example, a simulation package that
schedules events in simulated time could be implemented using the low-level
primitives. The simulation programs that made use of this package would be similar
to ordinary RED programs.
The following sections discuss high-level multitasking in more detail.
Low-level multitasking is discussed in Section 9.5.
7.2.2 ACTIVATIONS AND SCHEDULING
Each task activation is named by a single activation variable and has an
associated priority level (an integer between 0 and 255). The activation variable
can be used to set and query the priority of an activation. Within a priority level
activations are scheduled on a first-come first-served basis (255 is the highest
priority and is served first).
The activation variable can also be used to cause the X_TERMINATE exception to
be raised in an activation and to delay an activation for some specified period of
time. The X;TERMINATE exception makes it possible to terminate other tasks when
necessary. Tasks can be programmed to terminate gracefully (when X_TERMlNATE has
been raised in them) by restoring data in the external environment to a consistent
state.
Care has been taken in RED to ensure that there are no "dangling reference"
problems associated with activations. A dangling reference could occur if an
activation had access to a variable with a shorter lifetime than its own. RED
avoids dangling references through these provisions: '
- each actual VAR and READONLY parameter to a task must have a lifetime as
long as the lifetime of the task being activated,
- a scope cannot be exited until all activations of tasks declared in the
scope have terminated, and
- tasks may not have OUT formal parameters.
7.2.3 MESSAGE PASSING
One style of developing parallel programs is to separate the activations into
disjoint groups that share no memory. Since synchronization to share common data is
needed only within a group, and not between groups, the resulting program is easier
to understand and verify. Furthermore, the partitioning is often natural,
especially when the data are distributed on different computers of a network.
In such a structure, inter-group communication is accomplished by sending and
receiving messages. Since the different groups are independent of one another, it
is desirable that the message passing mechanism not require them to synchronize
their activities too closely.
RED provides a convenient and easily used form of inter-group communication by
means of mailboxes and the WAIT statement. A mailbox provides a buffer between the
sender and receiver. The size of this buffer, specified at the time the mailbox is
created, determines how closely coordinated senders and receivers must be. If it is
desired that the sender and receiver proceed precisely in step, then a mailbox of
size 0 may be specified.
Mailboxes provide send and receive operations for sending and receiving
messages, respectively. These operations can be invoked as stand-alone statements
or as clauses in the WAIT statement. The latter use is convenient, for example,
when an activation is waiting for messages from several mailboxes. In such a case,
each branch of the WAIT statement would invoke the RECEIVE routine for the
corresponding mailbox. The statement
could be used in a task that provides read and write access to a database. The read
and write requests will be handled on a first-come first-served basis, except in the
case of several requests waiting when the WAIT statement is entered. When this
occurs, an arbitrary choice will be made among requests.
A WAIT statement with multiple sends could be used to request a service from
multiple providers of that service; the first one to respond would be used. For
example,
will cause data to be printed on whichever printer is available first (or an
arbitrary choice will be made if both are available).
In addition to sends and receives, delays can be used as clauses in WAIT
statements. One use is to provide a "timeout", so that the waiting activation does
not wait indefinitely, or is awakened periodically to handle other responsibilities.
Delays can also be used to give priority to some mailboxes over others. For
example,
gives priority to requests coming in on the high priority mailbox.
Delays can also be invoked in contexts other than WAIT statements. Delays can
be used to provide periodic service and to allow an activation to wait until all
activations created by it are inactive.
7.2.4 DEADLOCKS
RED prohibits a send to a zero-length mailbox as a clause in a WAIT statement.
This prohibition assures that use of the WAIT statement will not cause cycles of
activations waiting to send and receive from zero-length mailboxes. If such cycles
occurred, the activations involved would be deadlocked; the cycles could be
prevented only by a global scheduling strategy. Such cycles can occur without
danger when mailboxes of length greater than zero are used, because senders and
receivers of a non-zero-length mailbox need not synchronize to exchange data.
7.2.5 DISTRIBUTED COMPUTING
RED mailboxes are a candidate for a communication mechanism in a distributed
network. The notion of ordering of sends and receives must be clarified for a
distributed environment, however. In particular, sends (receives) to a mailbox
should probably be ordered according to the time that they arrive at the computer
where the mailbox is stored, rather than the time they are actually sent. Such an
interpretation is consistent with RED mailboxes.
Since the area of distributed computing is still the subject of basic research,
it is possible that some applications may require a different set of primitives than
the RED mailbox facility. For example, when a mailbox is full, senders are queued
up (in the order that the sends arrived at the node containing the mailbox) and
given FIFO service. This decision may be inappropriate in a distributed environment
because of the inter-node communication that it implies.
RED's low-level multitasking facility provides the flexibility for the user to
define different message-passing primitives if this is needed. This flexibility
should be useful in adapting RED to the variety of requirements that may be
specified in the future.
7.2.6 REGIONS
Mailboxes provide a means of communication between activations that share no
memory. In addition, it is often useful for activations to communicate via shared
memory.
Regions and datalocks permit activations to synchronize their use of shared
memory. A datalock is associated with each set of related data that are to be
shared; the data in the set are then used only within a region controlled by the
datalock. This ensures that
- only one activation at a time can access the data (mutual exclusion); and
- the datalock is released when the region is exited, no matter how the exit
occurs.
Regions and datalocks are not strictly necessary, since mailboxes can be used
for synchronization. However, when mailboxes are used for this purpose the
programmer may initialize the synchronization improperly or forget to release a
resource when the critical section is finished. These errors can be avoided by the
use of regions.
Of course, regions and datalocks do not preclude all errors. For example, a
user might forget to limit access to shared data to a REGION statement. Also,
releasing the lock automatically when the region is exited does not ensure that the
shared data are usable: they might be left in an inconsistent state. To avoid the
latter situation, the programmer should limit the problems caused by exceptions (and
X_TERMINATE in particular) to as small a section of code as possible. For example,
Here, push is defined so that an inconsistent shared stack can never occur. When
such a solution is not possible, exception handling can also be used to help restore
data to a consistent state.
Another advantage of the REGION statement is that it enables the translator to
detect (some) potential deadlocks. This can be achieved by ordering the datalocks
according to their use in nested REGION statements; i.e., d1 < d2 if a REGION
statement locking on d2 is nested in a REGION statement on d1. If such an ordering
cannot be found, it indicates the danger of deadlock so that the translator can
issue an appropriate warning.