eberard@ajpo.sei.cmu.edu (Edward Berard) (10/15/89)
Earlier, Joe Pallas (pallas@Neon.Stanford.EDU) wrote: > Don't make object-oriented programming harder than it needs to be: if > your system has no concurrency, then you don't have to worry about > concurrency just because you're using object-oriented programming > techniques or languages. > On the other hand, if your system does have concurrency, > object-oriented techniques MAY help to deal with it. Or maybe > not---this is still open, I'd say. Before I begin, I must say that I agree with Joe when he says, in effect, if your system has no concurrency, don't introduce concurrency just for the sake of concurrency. However, one of the more attractive things about an object-oriented approach is that it encourages one to adhere to two of the most basic principles of engineering: 1. Make the solution look like the problem. 2. Form follows function. Said yet another way, object-oriented solutions are supposed to model the "real world." The closeness of our object-oriented solutions to the real world, of course, depends on a number of items, including: - the current level of abstraction, i.e., as we move to lower levels of abstraction, we tend to get further from the real world, and closer to the machine (computer), - the capabilities provided to us by our choice of programming language, e.g., some programming languages make it difficult to easily provide a "one-to-one mapping" to real world objects, - the requirements (restrictions) placed on the implementation by our clients, e.g., we may have to interface with an existing, non-object-oriented system, and - probably most importantly, the skill and intuition of the system designers and implementers. The real world is much more concurrent than you might expect. We are virtually surrounded by "objects with life" (i.e., objects which can, and do, change their state spontaneously). Examples of objects with life include people, temperature sensors, and electric alarm clocks. One could have modeled the recent OOPSLA meeting as almost 2,000 concurrent objects interacting randomly (;-)). Sometimes, those implementing object-oriented solutions either ignore the concurrency present in real life, or handle it in an unnatural manner. For example: - Many of the solutions presented at OOPSLA for the Greed game (see an earlier posting for the specification) ignored the fact that two, or more, dice can be rolled concurrently. These solutions artificially constrained the dice to be rolled individually. - Often "anthropomorphism" (interpretation of what is not human or personal in terms of human or personal characteristics) is inappropriately introduced into object-oriented solutions. This frequently creates additional (conceptual) threads of control where none existed in the real world. If the real world is concurrent, how do we properly introduce concurrency into our object-oriented solutions? Let's start with something simple and intuitive. Something that a 9 or 10 year-old child (literally) can understand. Consider the discussion of "agents" at the recent OOPSLA. Agents are composed of "agent rules." An agent rule represents a fairly atomic unit of behavior, e.g., eat, wander, and flee. Agent rules can be gathered into collections, i.e., agents, which represent larger (non-atomic) types of behavior, e.g., aggressiveness. Quoting Fenton and Beck ["Playground: An Object-Oriented Simulation System with Agent Rules for Children of All Ages" in the proceedings of OOPSLA '89.], "An agent rule is an independently executing entity sensitive to particular sign stimuli." Agents can be assigned to individual objects, with the result being objects which display non-deterministic, and "almost life-like" behavior. Please note that since agents represent multiple (independent and/or interacting) threads of control, you could view this as an example of concurrency which is _internal_ to an object. The Playground system (discussed in the Fenton and Beck article) is intended for use by children. According to the article, most of the children (who were 9 and 10 years of age) had few problems recognizing that real world entities could be doing two, or more things at once. For example, a fish could be eating and watching for predators at the same time. (At last, our objects can "walk down the street and chew gum at the same time." 8-)) Let's now shift the focus to a more technical level. In studying software reusability, people frequently make the mistake of assuming that the differences among reusable components are strictly functional. In the object-oriented community, reuse often takes the form of "robbing code" from a superclass, or a prototypical object. Grady Booch, on the other hand, saw things in a slightly different manner. Booch recognized that you could take the same basic abstraction (e.g., a list, a set, a stack, a matrix) and create a number of different "forms" for the abstraction. Each form represented a truly different class. Some of the forms were very simple, for example, "bounded" and "unbounded." Objects which were instances of classes representing bounded forms has a fixed upper limit to the number of items they could contain. Objects which were instances of classes representing unbounded forms were limited only by the computational resources (e.g., memory) as to the number of items that they could contain. Booch, of course, described forms which provided mechanisms for handling concurrent situations. In researching, and extending, these forms at my former company (EVB Software Engineering, Inc.), we came to realize that: - Object-oriented approaches offered a mechanism for handling concurrency which is fundamentally different from the more traditional approaches. In the more traditional approaches, a master routine (or series of master routines) was often charged with maintaining order (e.g., avoiding deadlock, starvation, and race conditions) in a system. In object-oriented systems, objects could be provided with a means of "handling themselves" in concurrent applications. This meant a shifting of responsibility, i.e., from the master routine to the objects themselves. If done well, this resulted in a simplification of the overall systems. - There are two, very different, basic forms of object-oriented concurrency: operational (or class) based concurrency, and object-based concurrency. In class-based concurrency, the mechanisms (methods) for dealing with concurrent situations were localized in the class itself. This meant that all concurrent accesses to the objects created using the class had to, in effect, go through the class. This has the effect of "locking out" any operations on other objects which are instances of the same class whenever an operation is attempted on an given instance of a class. Object-based concurrency, on the other hand, dictated that the mechanisms for handling concurrency were localized in the instances (objects) of the class, i.e., each object carries with it the capability to handle concurrent interactions. This means that interacting with one instance of a class has no impact on interactions with other instances of the same class. Thus, object-based concurrency provides more "true concurrency" than does class-based concurrency. The main tradeoff is this: Object-based concurrency requires more resources (e.g., CPU cycles and memory) than does class-based concurrency. Booch defined a number of concurrent forms for classes. We observed that these forms could be used with either class-based concurrency or object-based concurrency. We also added a form. These forms are: - "Concurrent" (we renamed this form to "protected"): Objects which were instances of "concurrent" classes handle concurrent interactions in a simple manner, i.e., they sequentialize all accesses to the object. This is not terribly concurrent, but objects "can take care of themselves" in a concurrent environment. - "Guarded": Suppose you want to create a "macro" using two, or more operations in the interface for an object. Further, you want to insure that no other operations take place with the object until your macro is complete. Guarded forms provide their users with a semaphore which enables a user to "lock" and object, perform the desired series of operations, and then "unlock" the object. This has, unfortunately, all the undesirable problems associated with semaphores, e.g., forgetting to unlock an object when you are through, "getting killed off" before you have a chance to unlock the object, and forgetting to ensure that all other entities abide by the semaphore rules. - "Multiple": Not all interactions with an object require a state change for that object. It may be entirely appropriate to allow multiple "readers" (i.e., interactions which cannot change the state of the object), while sequentializing "writers" (i.e., interactions which change the state of the object). [This is the classic "multiple readers and writers" concurrency problem.] Objects which are instances of classes of the multiple form allow multiple readers while sequentializing writers. (While conceptually very simple, there are many interesting points which must be appropriately addressed in the Multiple form. For example, readers must have access to information which is up-to-date.) - "Multi-Guarded" (we added this form): Multi-guarded is essentially a combination of multiple and guarded. There are many other important issues which can (and must) be discussed in the design of concurrent object-oriented systems. However, what is important to realize is that useful, concurrent object-oriented systems have been (and are being) constructed using these, and other principles. (This message is already too long. See you later, and thanks for listening.) -- Ed Berard (301) 353-9652