As a conclusion to this discussion, we will review the essential criteria that should guide the development of a concurrent O-O mechanism. These criteria served as a basis for the approach presented here; in a few cases, as will be seen, some more work remains to be done to achieve full satisfaction of these goals.
The following goals will be studied:
Object-oriented software construction is a rich and powerful paradigm, which, as noted at the beginning of this chapter, intuitively seem ready to support concurrency.
It is essential, then, to aim for the smallest possible extension. Minimalism here is not just a question of good language design. If the concurrent extension is not minimal, some concurrency constructs will be redundant with the object-oriented constructs, or conflict with them, making the developer's task hard or impossible. To avoid such a situation, we must find the smallest syntactic and semantic epsilon that will give concurrent execution capabilities to our object-oriented programs. The extension presented in the preceding sections is indeed minimal syntactically, since it is not possible to add less than one new keyword.
It would be unacceptable to have a concurrent object-oriented mechanism which does not take advantage of all object-oriented techniques, in particular inheritance. We have noted that the "inheritance anomaly" and other potential conflicts are not inherent to concurrent O-O development but follow from specific choices of concurrency mechanisms, in particular active objects and path-expression-like synchronization; the appropriate conclusion is to reject these choices and retain inheritance.
We have repeatedly seen how inheritance can be used to produce high-level classes (such as PROCESS) describing general behavioral patterns to be inherited by descendants. Most of the examples would be impossible without multiple inheritance.
Among other O-O techniques, information hiding also plays a central role.
It is also essential to retain the systematic, logic-based approach to software construction and documentation expressed by the principles of Design by Contract. The results of this chapter were indeed based on the study of assertions and how they fare in a concurrent context. In this study we encountered a striking paradox, the concurrent precondition paradox, which forced us to provide a different semantics for assertions in the concurrent case. This gives an even more fundamental place to assertions in the resulting mechanism.
A principle of object-oriented software construction was developed in preceding chapters: command-query distinction. The principle enjoins us not to mix commands (procedures), which change objects, and queries (functions and attributes), which return information about objects but do not change them. This precludes side-effect-producing functions.
It is commonly believed that the principle cannot hold in a concurrent context, as for example you cannot write
next_element := buffer.item; buffer.remove
and have the guarantee that the element removed by the second call is the same that the first instruction assigned to next_item. Between the two instructions, another client can mess up with the shared buffer. Such examples are often used to claim that one must have a side-effect-producing function get, which will both return an element and remove it.
This argument is plainly wrong. It is confusing two notions: exclusive access and routine specification. With the notation of this chapter, it is easy to obtain exclusive access without sacrificing the command-query distinction principle: simply enclose the two instructions above, with buffer replaced by b, in a procedure of formal argument b, and call that procedure with the attribute buffer as argument. Or, if you do not require the two operations to apply to the same element, and want to minimize the amount of time a shared resource is held, write two separate routines. This kind of flexibility is important for the developer. It can be provided, thanks to a simple exclusive access mechanism, regardless of whether functions may have side effects.
A general criterion for the design of a concurrent mechanism is that it should make it possible to support many different forms of concurrency: shared memory, multitasking, network programming, client-server computing, distributed processing, real time.
With such a broad set of application areas, a language mechanism cannot be expected to provide all the answers. But it should lend itself to adaptation to all the intended forms of concurrency. This is achieved by using the abstract notion of processor, and relying on a distinct facility (such as the Concurrency Control File) to adapt the solution to any particular hardware architecture that you may have available.
Many concurrency mechanisms have been proposed over the years; some of the best known were reviewed at the beginning of this chapter. Each has its partisans, and each may provide the best approach for a certain problem area.
It is important, then, that the proposed mechanism should support at least some of these mechanisms. More precisely, the solution must be general enough to allow us to program various concurrency constructs in terms of that mechanism.
Here the facilities of the object-oriented method should again be put to good use. One of the most important aspect of the method is that it supports the construction of libraries for widely used schemes. The library construction facilities (classes, assertions, constrained and unconstrained genericity, multiple inheritance, deferred classes and others) should allow us to express many concurrency mechanisms in the form of library components. Some examples of such encapsulating mechanisms (such as the PROCESS class) have been presented in this chapter, and the exercises suggest a few more.
One may expect that a number of libraries will be produced, relying on the basic tools described in this chapter and complementing them, to support concurrency models catering to specific needs and tastes.
As a special case, coroutines provide a form of quasi-concurrency, interesting both in itself (in particular for simulation activities), and as a smoke test of the applicability of the mechanisms, since a general solution should adapt itself gracefully to boundary cases. We have seen how it is possible, once again using the library construction mechanisms of object technology, to express coroutines based on the general concurrent mechanism.
It is necessary to support the reuse of existing, non-concurrent software, especially libraries of reusable software components.
We have seen (page 992 ) how smooth the transition is between sequential classes such as BOUNDED_QUEUE and their concurrent counterparts such as BOUNDED_BUFFER (just write separate class BOUNDED_BUFFER [G] inherit BOUNDED_QUEUE [G] end). This result is somewhat tempered by the frequent desirability of encapsulation classes such as our BUFFER_ACCESS. Such encapsulation seems useful, however, and may be an unescapable consequence of the semantic differences between sequential and concurrent computation. Also note that such wrapper classes are easy to write.
One area in which more work remains necessary is how to guarantee deadlock avoidance.
Deadlock potential is a fact of concurrent life. For example any mechanism that can be used to program semaphores (and a mechanism that is not powerful enough to emulate semaphores would be viewed with suspicion) can cause deadlock, since semaphores are trivially open to this possibility.
The solution lies partly in the use of high-level encapsulation mechanisms. For example a set of classes encapsulating semaphores, as was presented for locks ("Locks", page 978, with further ideas in <exercise 28.8), should come with high-level behavior classes which automatically provide a free for every reserve, thereby guaranteeing deadlock avoidance for applications that follow the recommended practice and inherit from the behavior class. This is, in my experience, the best recipe for deadlock avoidance.
This approach may not be sufficient, however, and it may be possible to devise simple non-deadlock rules, automatically checkable by a static tool. Such a rule --- expressed as a methodological principle rather than a language validity rule, for fear it may be too restrictive --- was given earlier (the Business Card principle). But more is needed.