Behavioral extension
ACT++
class extended_buffer : bounded_buffer {
behavior:
full_buffer = { get(), get_rear() };
partial_buffer = { get(), get_rear(), put() };
public:
int get_rear() {
reply buf[--in % MAX];
out %= MAX;
if (in == out) become empty_buffer;
else
become partial_buffer;
}
};
slide: Behavioral extension in ACT++
The class {\em extended_buffer} adds a method {\em get_rear}
and changes the behavioral abstractions {\em full_buffer}
and {\em partial_buffer} accordingly.
Behavioral abstractions provide a way
in which to specify synchronization
conditions and concurrency control for active objects.
First, behavioral abstractions characterize in a concise
way the possible state transitions of an object, and thus
allow reasoning about the correctness of the implementation
on a high level of abstraction.
Secondly, behavioral abstractions may serve to document
the protocol of interaction a client must comply with,
since they
specify the dynamic interface,
which determines which requests will be granted and
which postponed.
In [Neusius],
some refinements to decentralized concurrency control
as employed for ACT++ are proposed.
These refinements involve the possibility of specifying
more detailed matching procedures to determine whether
a particular method is available.
In addition, [Neusius]
gives some guidelines that must be followed
to employ inheritance for active objects in an optimal
way, for example:
{\em when we redefine a method
in a subclass we must be able to refine this within
the concurrency control specified in the superclasses}.
Evidently, active objects require careful design.
As yet, no fully developed design method
for active objects exists.
Multi-threaded active objects
Inheritance is a valuable mechanism
for incremental process specification.
In a similar way as for ordinary object classes,
inheritance may be applied to processes
(read active object classes)
to achieve better conceptual modeling,
and code reuse by factorization.
Stepwise refinement may be a helpful technique
to structure the development of
complex processes.
In [Tom87], an example is given of how multiple
inheritance may be used to define
distributed termination detection algorithms
in a modular, incremental way.
The idea is to define the normal behavior of
the (combined) process first
and add exceptional behavior by means of
a subclass.
To complement the resulting process with termination
detection, a separate process is defined
for this task which is combined with the process
specifying the actual computation by using multiple
inheritance.
A class that contains functionality
pertaining to processes in general may be placed
at the root of the inheritance graph,
as shown in slide [6-dtd].
The problem of distributed termination detection
is to determine whether all processes
participating in the computation have terminated
and that no messages are still waiting to be answered.
The solution to distributed termination detection
gives an interesting
application of (multiple)
inheritance on processes.
We will, however, not go into details
of the solution.
Instead, we discuss the requirements
the implementation language must meet
to support the incremental development
of distributed algorithms.
In [Tom87] a sketch is given of an object-oriented
language supporting the constructs needed
to implement the design pictured above.
Apart from the ordinary constructs for supporting
object-oriented programming
(such as classes and attributes),
the language
supports active objects that may contain multiple action
parts and communication by means of CSP-like send
statements.
Action parts inherited from ancestor classes
result in processes that run in parallel with
the process associated with the object itself.
In other words, an active object may consist
of multiple parallel (lightweight) threads.
The priority with which a process runs decreases
with the length of the inheritance chain.
The process associated with the actual object itself
has the highest priority.
Ancestor processes run with a lower priority
as they are higher up in the inheritance graph.
Communication between objects takes place
in a CSP-like manner by asynchronous send
and receive statements.
A receive statement may be preceded by a guard.
Each object must specify which messages
it is willing to receive, by means of a non-deterministic
select statement.
Guards may be used to ensure that, on acceptance of
a message, certain conditions are fulfilled.
The precise details of the language
need not concern us here.
However, we must note that any language supporting the
incremental development of distributed algorithms
must support both multi-threaded
active objects and guard constructs
that allow for the conditional acceptance
of messages.
Inheriting behavior -- Active C++
To inherit behavior in a concurrent context
may imply the inheritance of processes defined
by ancestor classes.
Semantically, inheritance involving processes may
be justified by observing that process inheritance
results in (strictly) adding behavior.
To finish this section, we will discuss the way
in which class inheritance is dealt with in Active C++.
Active objects in Active C++ may have multiple threads.
Multiple threads may arise either
when deriving an active class from another active class
or from specifying multiple active members.
An example of the latter situation is presented in the code
fragment in slide [6-acc-multi].
active class P {
...
public:
P() { ... }
active m1 () { ... }
active m2 () { ... }
};
slide: Multi-threaded objects in Active C++
The constructor of this class is passive.
The decision to allow ordinary member functions
to be active is motivated by the
consideration that constructors must take care of the initialization
of an object and not necessarily of its activation.
Multiple threads may also arise from using inheritance.
For example, assume an active class bounded (buffer)
as defined in section [active], yet allowing for multiple elements.
We will sketch the definition of an active class special (buffer)
which allows elements to be taken from the rear.
See slide [6-acc-beh].
active class special : public bounded {
public:
special() : bounded() {
for(;;) accept( rear:used>0 );
}
int rear() { ... }
};
slide: Inheriting behavior in Active C++
When creating a {\em special_buffer}, the constructor of
{\em bounded_buffer} will be called, which results in a process
willing to accept put and get.
In addition, a process is created with a conditional accept
statement, specifying that {\em get_rear} may be accepted whenever
the instance variable used is greater than zero.
For this solution to work,
we have to specify the body of the constructor of the
bounded_buffer ancestor class as
for (;;) accept( put : used < size, get : used > 0 );
employing accept expressions of the form ,
stating that acceptance is dependent on the function invoked
as well as on the satisfaction of the condition.
This is needed to enable the selection of incoming
messages to be dependent on the actual value
of the instance variable used.
To allow for the incremental specification of behavior we must
impose the following restrictions on the semantics of the accept statement:
- The acceptance conditions of multiple accept statements
(in multiple processes)
must be combined to form a single set of acceptance conditions.
- While a rendezvous takes place, no other member call may interfere.
- The process containing
the appropriate acceptance condition may proceed
after the rendezvous is completed.
(When there is ambiguity with respect to the identification of the process,
the runtime system must make a choice).
With these restrictions we think to have provided a (partial) solution
to the inheritance anomaly signaled in section [issues].
However, more research is needed to establish that the
solution is valid in all conceivable circumstances.