Objects and processes


Instructors' Guide
Parallel processing has an impressive history, both in a practical sense (one may say that processes existed before objects came into life) and in terms of the literature it has generated. (See, for example Andrews, 1991.) Yet the development of parallel (and distributed) programs is generally considered to be an expert's job, due to the inherent complexity of parallel solutions and the synchronization involved. Not surprisingly, the object-oriented community has taken an interest in parallel computing, hoping that the intrinsic complexity of parallel processing could be better managed by an object-oriented approach. In the following, we will first explore by what means we may introduce parallelism in an object-oriented system or language and, subsequently, what encapsulation is needed for processes and distribution in addition to the encapsulation already supported by the object model.

Language support

The shift towards distributed systems, supporting concurrency and communication between (geographically) distinct objects requires support that is not found in the object-oriented languages we have discussed thus far.

Language support -- distributed systems


slide: Language support

Usually, distributed applications rely on the primitives provided by the underlying operating system. However, the use of operating system primitives for process creation and communication is usually rather ad hoc and, consequently, error-prone and difficult to port across platforms. Fortunately, the object model seems to lend itself in a quite straightforward way to a generalization supporting distribution and concurrency. In the sections that follow, we will take a closer look at the notion of processes and how it relates to our common notion of objects. Apart from processes, an object-oriented language supporting distributed/concurrent computing also needs to provide for synchronization and communication primitives that fit within the message passing model of object-oriented computing. See slide 6-support. Finally, to support truly distributed programming, issues of fault-tolerance need to be dealt with as well.

Object-based concurrency

When it comes to combining objects (the building blocks in an object-oriented approach) with processes (the building blocks in parallel computing), there are three distributed approaches conceivable. See slide 6-o-conc.

Object-based concurrency


slide: Objects and concurrency

One can simply add processes as an additional data type. Alternatively, one can introduce active objects, having activity of their own, or, one can employ asynchronous communication, allowing the client and server object to proceed independently.

Processes

The first, most straightforward approach, is to simply add processes as a primitive data type, allowing the creation of independent threads of processing. An example is Distributed Smalltalk (see Bennett, 1987). The disadvantage of this somewhat naive approach, however, is that the programmer has full responsibility for the most difficult part of parallel programming, namely the synchronization between processes and the avoidance of common errors such as simultaneously assigning a value to a shared variable. Despite the fact that the literature (see Andrews, 1991) abounds with primitives supporting synchronization (such as semaphores, conditional sections and monitors), such an approach is error-prone and means a heavy burden on the shoulders of the application developer.

Active objects

A second, and in my view to be preferred, approach is to introduce explicitly a notion of active objects. Within this approach, parallelism is introduced by having multiple, simultaneously active objects. An example of a language supporting active objects is POOL (see America, 1987). Communication between active objects occurs by means of a (synchronous) rendezvous. To engage in a rendezvous, however, an active object must interrupt its own activity by means of an (Ada-like) accept statement (or answer statement as it is called in POOL), indicating that the object is willing to answer a message. The advantage of this approach is, clearly, that the encapsulation boundary of the object (its message interface) can conveniently be employed as a monitor-like mechanism to enforce mutual exclusion between method invocations. Despite the elegance of this solution, however, unifying objects and processes in active objects is not without problems. First, one has to decide whether to make all objects active or allow both passive and active objects. Logically, passive objects may be regarded as active objects that are eternally willing to answer every message listed in the interface description of the object. However, this generalization is not without penalty in terms of runtime efficiency. Secondly, a much more serious problem is that the message answering semantics of active objects is distinctly different from the message answering semantics of passive objects with respect to self-invocation. Namely, to answer a message, an active object must interrupt its own activity. Yet, if an active object (in the middle of answering a message) sends a message to itself, we have a situation of deadlock. Direct self-invocation, of course, can be easily detected, but indirect self-invocations require an analysis of the complete method invocation graph, which is generally not feasible.

Asynchronous communication

Deadlock comes about by synchronous (indirect) self-invocation. An immediate solution to this problem is provided by languages supporting asynchronous communication, which provide message buffers allowing the caller to proceed without waiting for an answer. Asynchronous message passing, however, radically deviates from the (synchronous) message passing supported by the traditional (passive) object model. This has the following consequences. First, for the programmer, it becomes impossible to know when a message will be dealt with and, consequently, when to expect an answer. Secondly, for the language implementor, allocating resources for storing incoming messages and deciding when to deal with messages waiting in a message buffer becomes a responsibility for which it is hard to find a general, yet efficient, solution. Active objects with asynchronous message passing constitute the so-called actor model, which has influenced several language designs. See  [Agha].

Process-based encapsulation

In his seminal paper on the dimensions of object-oriented design,  [Wegner87] observes that various notions of encapsulation, as have become popular with object-oriented programming, already existed in one form or another in the distributed programming community. To establish what requirements an object-oriented system must meet to qualify as (potentially) distributed, we once more rely on the analytical work of  [Wegner87], delineating precisely the notions of processes, threads and distribution. A process, according to  [Wegner87], consists of an interface (naming the possible transactions, that is operations, allowed) and one or more threads (that may be active or suspended). A thread is characterized by  [Wegner87] as consisting of a locus of control (in other words, a program counter) and an execution state (consisting of a runtime stack and possibly a heap for dynamically created data). See slide 6-threads.

Taxonomy of processes

  • process: interface + active/suspended thread(s)
  • thread: locus of control + execution state
  • sequential process -- one thread of control
  • quasi-concurrent -- at most one active thread
  • concurrent -- multiple active threads

slide: Processes and threads

Dependent on the number of threads, we may characterize a process as either sequential (a single thread of control), quasi-concurrent (at most one active thread, arbitrarily many suspended) or (truly) concurrent (multiple active threads, arbitrarily many suspended). When considering active objects, a distinction between single threaded objects and multi-threaded objects seems to be more appropriate. The most straightforward approach is to associate an object with a single thread of control, that may be divided between answering messages and the object's own activity. The model of communicating sequential processes (corresponding to single threaded objects) was first advocated in  [Ho78]. It underlies programming languages such as CSP, occam and Ada, and is probably the best understood model of parallel processing that we have (see Andrews, 1991). In combination with the assumption that objects share no variables, single threaded objects bear a close resemblance to what  [Wegner87] calls distributed sequential processes. However, with reference to the language DLP (which employs multi-threaded objects to support distributed backtracking), we must note that single-threaded objects are not the only means to implement active objects. Distributed sequential processes, as characterized by  [Wegner87], provide a combination of characteristics that make them exemplary as a model for active objects. See slide 6-seq-proc.

Distributed sequential processes -- own address space

  • Unit of modularity -- user interface
  • Unit of concurrency -- single thread
  • Unit of naming -- name space

slide: Sequential processes

Perhaps their most important characteristic is the absence of global data shared with other processes. As a consequence, they provide what may be called strong encapsulation, since in addition to the absence of global variables, the transaction interface of a process precisely delimits the functionality offered to potential clients. In addition to the protection with regard to global variables, distributed processes must satisfy a number of other requirements to guarantee correct execution. These requirements will be dealt with in more detail in section distribution.

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. }