Inheritance on processes

Active objects provide a convenient way in which to employ concurrency in object-oriented systems. Unfortunately, active objects and inheritance do not easily sit side by side, as observed by a number of authors. See, for example,  [Am87a],  [Briot87] and  [Mats93]. The objections to employing inheritance for active objects are based on semantical considerations, as well as on pragmatic reasons concerning the reuse of synchronization and acceptance conditions. The latter problems are commonly referred to as the inheritance anomaly. See  [Mats93] for an in-depth analysis. In this section we will analyze the nature of the incompatibility between active objects and inheritance and explore two solutions, namely the use of behavioral abstractions (as proposed for the actor model employed in ACT++) and the support for multi-threaded active objects (allowing the processes derived from ancestors to be combined).

Behavioral abstractions {\em -- the inheritance anomaly}

Concurrency control for active objects is needed to indicate when the object is to interrupt its own activity and to determine which messages are eligible to be answered. See slide 6-anomaly.

Concurrency control

-- acceptance conditions
slide: Concurrency control

In many object-based languages supporting active objects, such as POOL-T (America, 1987a), concurrency control is centralized in a single main process routine of the object (called the body in POOL-T) by means of accept or answer statements listing the methods for which a request may be granted. As observed in  [Am87a], when inheriting from an active object reuse does not apply to the main process routine, since in general synchronization and acceptance conditions will be quite dissimilar. (Which was a reason not to support inheritance in POOL-T.) A quite different approach to concurrency control is to adopt a decentralized protocol, in which the synchronization and acceptance conditions are indicated per method. Such a protocol specifies what conditions hold after the method has been executed. Decentralized concurrency control is the mechanism chosen for ACT++. Each method in an actor object (in ACT++) issues a become statement to characterize its (replacement) behavior, that is its behavioral capabilities according to the dynamic interface specified by the (sub)type expression in the become statement. Originally, behavioral transformations of this kind were specified in ACT++ by subclasses of the actor class. However, as observed in  [Kafura89], when adding a single method to an actor class by inheritance, the complete class structure characterizing the behavioral transformations of the original actor class must be rewritten to accommodate the newly added method. For example, when a method {\em get_rear} is (incrementally) added to a {\em bounded_buffer}, the synchronization conditions for {\em partial_buffer} and {\em full_buffer} must accordingly be changed. See slide 6-act-ba.

Behavioral abstractions

\zline{\fbox{ACT++}}
  class bounded_buffer : actor { 
   int buf[MAX]; int in, out;
  behavior:
   empty_buffer = { put() };
   full_buffer = { get() };
   partial_buffer = {get(),put()};
  public:
  
  bounded_buffer() { in=0; out=0; }
  
  int get() {
    reply buf[out++];
    out %= MAX;
    if (in == out)
       become  empty_buffer;
    else
       become partial_buffer;
  }
  
  void put( int item ) {
    buf[in++] = item;
    in %= MAX;
    if (in == out )
       become full_buffer;
    else
       become  partial_buffer;
  }
  };
  

slide: Behavioral abstractions in ACT++

To remedy the inflexibility of their original approach, the designers of ACT++ proposed the use of behavioral abstractions (which are roughly sets of enabled methods) instead of (sub)classes to specify the behavioral capabilities of an object in a become statement. As an example, in the {\em bounded_buffer} specified above, the behavioral abstractions, characterizing the various states an active object may be in with respect to synchronization, are specified in a separate behavior section of the actor object as subsets of the available methods (as listed in the public interface description). In a way similar to virtual member functions, behavioral abstractions may be redefined by a derived class, as illustrated in slide 6-act-ext.

Behavioral extension

\zline{\fbox{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.

Distributed termination detection

}{}
  • \mbox{}\hspace{0.1cm}\ifsli{\vspace{1cm}}{} \parbox{5cm}{ \begin{mfpic}[20][10]{0}{10}{0}{10} \label[cc]{5}{9}{\c{distributed} process} \label[cc]{2}{7}{normal \c{computation}} \label[cc]{2}{4}{\c{computation with} exception} \label[cc]{5}{1}{final \c{process}} \label[cc]{7}{7}{\c{termination} detection} \line{(4.5,8.5),(2.5,7.5)} \line{(2.5,6.5),(2.5,4.5)} \line{(2.5,3.5),(4.5,1.5)} \line{(5.5,8.5),(6.5,7.5)} \line{(6.5,6.5),(5.5,1.5)} \end{mfpic} }

slide: Distributed termination detection

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 template : condition, 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:
  1. The acceptance conditions of multiple accept statements (in multiple processes) must be combined to form a single set of acceptance conditions.
  2. While a rendezvous takes place, no other member call may interfere.
  3. 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.