On the notion of active objects

Active objects are objects with a thread of their own. This definition is minimal in the sense that it does not restrict active objects to having only one thread. Neither does it specify what communication primitives must be provided and how synchronization (to avoid simultaneous access to shared data) is effected. As a starting point, we will present some examples of active objects, taken from Eliëns and Visser (1994). We will use these examples to illustrate some of the problems involved in combining objects and concurrency, and to discuss some of the criteria by which to compare the various language proposals dealing with concurrency and distribution. The most straightforward, although somewhat naive, approach to supporting active objects (in C++) is to extend the constructor mechanism with a facility for process creation. Consequently, in addition to the initialization of an object, the constructor (of an active object) creates a thread that may be employed either for the object's own activity or to respond to a message. As an example of a class specifying active objects, look at the definition of an active counter in slide 6-acc-objects

Active objects

\zline{\fbox{Active C++}}
  active class counter { 
\c{\zline{\fbox{Active C++}}}
private: int val; public: active counter( int n ) { val = n; for(;;) accept (operator++ , operator() ); } void operator++ () { val++; } int operator() () { return val; } };

slide: Active objects in Active C++

In the example, the keyword active has been used to indicate that the class counter defines active objects and, in addition, to indicate that the constructor of the counter creates a process thread. The counter class own activity presented is quite trivial. After initialization of the instance variable n, holding the actual value of the counter, a counter object enters a loop indicating its (eternal) willingness to execute either of the operators constituting the method interface. For the moment, we just assume that invoking a member function results in a rendezvous and provides complete mutual exclusion between member function calls, to guarantee the absence of simultaneous assignment to the instance variable n. Interestingly, despite its simplicity, the example contains a number of problems for which a language designer (and implementor) must provide a solution. The first, perhaps most fundamental, problem to note is that (taking the normal semantics of C++ constructors for granted) the constructor never terminates. One possible solution is to distinguish between the initialization of the object (for which naturally the constructor will be used) and the creation of a process (for example by a system-defined virtual function main()). However, an alternative solution would be to return a pointer to the object immediately, but to grant the constructor sufficiently high priority to guarantee that it finishes the initialization, until it blocks to wait for a request. As an aside, the latter solution would allow classes to inherit from classes defining active objects. See section conc-inheritance. Another problem, that is not of fundamental importance but which may highly influence the convenience with which the programmer may employ active objects (in combination with passive objects), is the extent to which the language constructs dealing with concurrency affect the programming style used to define (ordinary) passive objects. There are two sides to this question. First, from the point of view of the client of an object, there should (ideally) be no distinction between dealing with a passive object and dealing with an active object. This includes the creation of an object and the invocation of member functions. However, in some circumstances the programmer may need to influence where the newly created object will be located and possibly with what priority it must run. Secondly, to allow a gradual transition from a system consisting of passive objects to a system containing (some) active objects, the code defining the synchronization and communication properties of an active object should be as non-interruptive as possible. Naturally, whether a (collection of) language construct(s) must be regarded as non-interruptive is a highly qualitative judgement. It is merely used to stress the importance of a gradual transition from passive to active code, which seems to fit best within the incremental nature of object-oriented programming.

Communication by rendezvous

Again, the most straightforward way to deal with member function invocation in a concurrent setting is to regard it as a synchronous rendezvous between (possibly remotely located) objects. Synchronizing the communication between objects may then be done by using accept statements, as illustrated in the bounded buffer example given in slide 6-acc-rv.

Communication by rendezvous

\zline{\fbox{Active C++}}
  active class buffer {
  private:
     item it;
  public:
  
     item get () { return it; }
     void put (item i) { it = i; }
  
     active buffer () {
        do {
  		accept( put );
  		accept( get );
        } while (1);
     }
  };
  

slide: Communication by rendezvous

The acceptance of put and get is serialized to ensure that there is an item in the buffer when get is invoked. This buffer can be easily generalized to a bounded buffer containing a number of elements. Acceptance of requests then depends upon the internal state of the buffer object, that is whether the buffer is empty, full or somewhere in-between, as illustrated in the code slide 6-acc-synch.

Synchronization

  if (used < size && used > 0) accept(put,get)
  else if (used == 0) accept(put)
  else accept(get);
  

slide: Synchronization in Active C++

\c{ The synchronous rendezvous provides a quite well-established parallel programming paradigm, familiar from Ada. However, it does not necessarily lead to the most optimal solution with respect to exploitation of the concurrency potentially available. As another problem, and a fortiori this holds for C++ with its rather elaborate arsenal of data types, in a distributed environment provision needs to be made to transport arbitrarily complex data types across a network. } \c{ The examples looked at thus far are taken from an experimental language, Active C++, developed by the author's group as a vehicle for research in distributed/concurrent computing in C++. In the next section, we will look at a number of alternative proposals for extending C++ with concurrency and distribution. }