Výsledky vyhľadávania - Programming Techniques—Object-oriented Programming

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20

    Popis súboru: application/pdf

    Relation: Technical report (University of Alabama at Birmingham. Department of Electrical and Computer Engineering); 2018-07-ECE-023; Technical report (Southern Methodist University. Department of Computer Science and Engineering); 88-CSE-17; Technical Report 2018-07-ECE-023 Technical Report 88-CSE-17 Object Oriented Programming in a Shared Memory* Multiprocessing Environment M. G. Christiansen Murat M. Tanik S. L. Stepoway This technical report is a reissue of a technical report issued March 1988 Department of Electrical and Computer Engineering University of Alabama at Birmingham July2018 Technical Report 88-CSE-17 OBJECT ORIENTED PROGRAMMING IN A SHARED MEMORY• MULTIPROCESSING ENVIRONMENT M. G. Christiansen M. M. Tanik s. L. Stepoway Department of Computer Science and Engineering Southern Methodist University Dallas, Texas 75275-0122 March 1988 * This research was supported in part by the DARPA under contract MDA903-86-C-0182. Object Oriented Prograrruning in a Shared Memory Multiprocessing Environment M. G. Christiansen M. M . Tanik S. L. Stepoway Southern Methodist University Department of Computer Science and Engineering Dallas, Texas 75275 ABSTRACT A programming methodology for parallel applications is presented that relies on object oriented programming techniques. This work differs in that the object oriented techniques are applied to a shared memory multiprocessing environment. These techniques are used to implement shared data stuctures and processing agents. The use of a statically typed object language provided excellent execution times and speedups for multiple processing elements. A case study is presented in which the object oriented approach is compared to Fortran producing better performance and easiler understood source code. I. Introduction Object Oriented programming techniques have been recognized as effective design methodology for software system . In this paper we describe how object techniques can be applied to the design of parallel processing applications . Object oriented programming has been successfully applied in distributed programming languages [2] and operating systems [1]. Our emphasis is on the multiprocessing environment provided by a shared memory architecture [3]. We will describe how a statically typed object language can be used to design and implement cooperating processes, and shared data structures. Our work centered around the use of the C++ [4] programming language on a shared memory multiprocessor. C++ is a language, based on the C programming language [ 5], which has been extended to provide object oriented capabilities. These object features have been integrated with the multiprocessing capabilities provided by our Sequent Balance 8000 [ 6] to This research was supported in part by the DARPA und er co ntract MDA903-85-C-0182 - 2- form an effective parallel programming environment. In the following section we present a overview of object programming techniques . This is followed by a description of how these techniques are applied to parallel processing in a shared memory environment. Finally we describe a comparative case study of an application developed in C++ and Fortran using multiprocessing techniques. 2. Object Oriented Programming Techniques Object oriented design is often described as a declarative methodology [7], where an application is developed as a set of cooperating software objects. These objects encapsulate resources or functionality required by the application. The encapsulation is provided by a set of operations and data structures that implement the object. In this section we describe the important aspects of the object oriented paradigm. This discussion centers around object members, initialization, and inheritance. Object languages provide constructs that allows the declaration of the operations and data structures in a single programming unit. In the languages Smalltalk [8] and C++ it is termed the Class construct. In Ada [9] it is called the package. This is an example of a class construct in C++ that implements a stack object: class Stack { item stackBody[ stackSize]; int topOfStack; public: }; Stack(); I I Creator method -stack(); I I Destructor method void push( item); item pop(); This example demonstrates both the data structures and operations provided by the class - 3 - construct. Notice that this is only a class declaration, and the actual definition of the push and pop operation is presented later. When the stack class is used in an application , it is treated much like a user defined type . Instances of stacks can be declared as: Stack myStack; I I Declare an instance of Stack myStack.push( val); I I Push an item onto the stack val= myStack.pop(); I I Pop the value off Notice that the operations are associated with the instance myStack. In C++ the components of a class are referred to as its members. Thus the operations are called the member functions , and data structures are called its member variables. A member can be public or private. A public member can be accessed by clients that declare and use the services provided by the object. Private members can only be accessed by a member function . In the above example the push and pop operations were declared public and statements shown above are legal. But if the programmer attempted to access the topOfStack variable as in: myStack.topOfStack= 0; it would be flagged as an error by the compiler. The public members of the class definition provide an interface to the object. By restricting the access to an object to its interface we are able to hide the implementation of the object from the client. This disallows unrestricted access to the internal data structures, and provides robust implementations. This feature is especially important in multiprocessing applications where mutual exclusion is an issue . Another important feature of object languages is the ability to define methods that initialiZe and destroy instances of objects . In C+ + these are called the creator and destructor methods , and examples are also presented in the class Stack. The creator method initializes instances of the object before the client is allowed to operate on them . Likewise, the destructor - 4 - method allows the instance to be processed before it is -removed. We have found that these operations have been crucial in the maintenance of shared memory segments for interprocess communication. Object languages like 0+ + provide the ability to combine existing class definitions to form new composite classes. These composite classes provide the ability further partition large object definitions into more manageable pieces. These smaller class definitions can also be used in multiple classes providing for code reuse. An example given in appendix A shows a class definition for a lock at line 11. The underlying lock mechanism is provided by the operating system, and the member functions allow an instance to be either locked or unlocked atomically. This lock class is later used in the declaration of the sharedStack class at line 24 . The member functions pop (line 43) and push (line 35) use the instance of lock allocated to the stack instance to provide exclusive accesses to the stack 's internal data structures. 3. Concurrent Programming in 0+ + In this section we discuss how 0+ + has been used to ease the burden of designing and implementing concurrent applications. This aid has been in two forms. The first is the use of 0+ + classes in the definition and reuse of shared (and nonshared) data structures. These data structures include stacks, lists, queues, locks, counting semaphores , and others. Many of these structures can , and have been, implemented as abstract data types, where the programmer declares an instance of the class and operates on that instance through the provided interface . The second is the application of object centered techniques in the design of applications. The model of an object as an independently executing entity lends itself well to task partitioning in concurrent processing. - 5 - 3.1. Shared Objects and Data Structures When programming a shared memory multiprocessing system, most if not all, interprocess communication takes place in memory shared between two or more pro cesses. Other forms of communication exist such as pipes and message queues , but these require a certain amount of overhead that is not acceptable in many cases. Shared memory is very fast but also very primitive . It provides no method of supporting mutual exclusion, sequencing, or synchronization. Sequent provides a fast hardware locking mechanism, called Atomic Lock Memory, that can be used with shared memory to implement higher level data structures such as shared arrays, stacks, lists , and queues . A requirement of these data structures is that they provide the needed exclusion so that concurrent accesses are properly resolved. Even when developing systems in conventional uni-processor environments, supporting the data structures mentioned above can be tedious and error prone . The software engineer must not only integrate these data structures into their applications at a conceptual level, but must be aware of the underlying implementation details that might include boundary and overflow conditions as well as storage allocation and de-allocation. In a multi-processor environment the implementation details include the allocation of semaphores, deadlock avoidance , assurance of mutual exclusion, and other details that are implementation dependent. The use of objects can partition many of these implementation details to the definition of the object's class . The definition of the class can use inheritance of other object definitions to further partition the complexity. Appendix A contains an example of a shared stack object that can be accessed by multiple processes. This example , although limited, expresses most of the key features we have mentioned. The class sharedStack (line 21) contains private and public members. All members defined before the "public:" label are private and only accessible by the class 's own members. The remaining members are public, and accessible by clients of the class . The first two members of the public section are the class 's creator and destructor methods . Notice the creator method, sharedStack() (line 28) makes use of a variable named - () - "this". The variable is a pointer to the location of the instance of the class (the object) in memory. By assigning to "this" in the class's creator method, we are able to define the method of storage allocation used in allocating the needed space. In this case we are using the shared memory allocator shmalloc() , which returns a pointer to storage in the shared memory segment. This allows child processes to access this storage. Likewise the destructor member function -sharedStack() first de-allocates the shared memory and sets "this" to null. Another interesting feature of this example is the use of the class Lock (line 11) . This class defines objects that use the Atomic Lock Memory to implement a mutual exclusion mechanism. The class's only private member is the pointer to the hardware lock of type slock_t. The three public members initialize , lock, and un-lock the lock. This is a blocking operation in which the process attempting to unlock a locked lock will be blocked until the lock is un-set by some other process. Note the use of the lock in the sharedStack member functions push() and pop() . In these functions the lock is called with no reference to the underlying implementation of the slock_t variable . Any number of locks can be declared in this way. The creator method for lock is called automatically when an instance of the class sharedStack is declared. In the main() of the routine (line 64) we see that an instance of the class sharedStack is created when the class is used as a type (line G7) . Then a number of child processes are created using the function mfork() (line 71) . Notice that the child processes are passed a reference to the shared stack. The function funcl() (line 75) will operate by popping an item off the stack, dividing it by two, and checking if it is equal to one. If not, two new items are pushed back onto the stack. If equal to one , a. message is printed. This continues until the stack reports that it is empty. Notice that the funcl need not concern itself with the details of dealing with the lock. Because we have hidden the underlying locking mechanism in the class definition, there is little difference between this stack object and the simpler version that would have been required in a uni-process application. Although space would not allow other examples to be included, we have used these ideas r - 7 - to create other shared data structures such as: arrays, linked lists, counters, queues, and other hybrid structures that use inheritance in defining their behavior [13]. 3.2. ConcUITent Processing Agents An advantage in using an object oriented approach in software development is the ability to encapsulate data structures and procedures in classes that represent a concept being implemented. Actors [ 10], an object oriented distributed programming language, is entirely based on the model of communicating processing entities which exchange messages, and use procedures called scripts to determine a response to a received message . The encapsulation of a concept in the definition of an object, and its implementation as a independently executing agent is a key strategy in developing concurrent applications . We have applied two strategies in the definition of processing agents for multiprocessing applications. The first is to apply a pipeliuing approach, in which each agent performs a part of the total operation performed on a data item, or at each iteration in the program. The second is to apply several identical agents concurrently to the problem, where each agent fully processes a data item or iteration. This second approach has the advantage of allowing us to more easily apply a variable number of processing elements to a given task. \7Vhen defining objects it is important to find a natural functional partition between concepts present in the application. A natural partition is one in which that amount of communication and synchronization is kept to a minimum. This will allow each independently executing process to function with a minimum of contention for shared resources. But often there is a trade off between object partitioning and efficiency of execution. Even in C++ where message passing is implemented using function calls, the overhead is too great for instances where high object interaction is required. In these cases it is often necessary to allow an object direct access to the data structures contained in another object with which a great deal of interaction is necessary. This brings to our attention a strategy m system development that IS supported by the - 8 - object oriented approach . This is the incremental development of applications using objects. This strategy involves the development of a system in two stages . In the initial stages the system is defined and partitioned into a collection of loosely coupled objects that interact with each other through their interfaces . This facilitates the decomposition of the system into its basic components and helps to identify components that are either poorly defined or improperly implemented. Once the system has been properly prototyped using loosely coupled objects, a profiling of its execution can be developed to locate areas of potential optimization . This optimization will be aided by the experience and insight gained in the initial development. A result of the optimization quite likely will be a tighter coupling between objects that are highly interactive. Because of the partitioning of the objects in the initial development, the optimization can be achieved with less of the problems due to unseen side effects of the changes that are made. Appendix B contains an example of an agent object that creates an independently executing child process when an instance is declared. The class D erived1 (line 17} is derived from the class Process (line 10}. When the Process object is initialized, it performs a fork() assigning its public member forkReturn to the value ~eturned. The derived class uses forkReturn to determine whether to return to the main (the parent process) or call the "work(}" member function (the child process}. In this example the member function work(} (line 29} is extremely simple, but we have constructed cases where this fun ction has access to the shared data structures described in the previous section . These shared structures are used as interprocess communication channels between processes, and the agents are defined to perform some subtask in the application. 3.3. An Application of Parallel Object Oriented Techniques: The Auction Algorithm The object paradigm of programming is often criticized as being too inefficient due to the overhead of message passing between cooperating objects . We feel that this overhead can be minimized, while retaining acceptable object partitioning by judiciously tightening object coupling where high bandwidth inter-object communications are needed. - 9 - As an example we present a case study of applying parallel object oriented techniques in developing a non-trivial application using 0+ + . The subject of the case study is the implementation of the Auction Algorithm [11] . The algorithm provides a solution to the linear cost assignment problem . It provides a method by which two sets of N items are mapped onto each other by means of a cost function, so that the resulting assignments are optimal. This mapping is one to one, so that each item in set A is assigned to a unique item in set B. This process has the metaphor of an auction, where competing agents bid against each other for assignment to resources . Each agent has a cost function that is applied when determining an item to bid upon, and how much the bid should be. The cost function defines the "worth" of an item to the agent. The agent uses the cost function minus the current price of an item to select the item that maximizes its profit. As an item receives a bid, it is compared to its existing offer and accepts if the new bid is greater than its current offer. If accepted, the bidding agent is made the new owner of the item, and any previous owner is notified its offer was declined where it assumes an unassigned status. Only unassigned agents participate in the bidding process. This continues until all agents have been assigned to a single item . The unique feature of this algorithm is that the bidding process for each unassigned agent can occur in parallel. Furthermore, each agent can bid asynchronousally in relation to the bidding processes of other assigned agents. We refer to this as a chaotic implementation, as agents access pricing information and make bids in an unrestricted manner. Although an agent might select an item based on out-dated pricing information because of the concurrent nature of the application, this bad decision will later be nullified, and the optimal solution reached. The parallel 0+ + implementation of this algorithm defines two major classes. The first are the agent objects that can concurrently decide upon what item to bid next. The second are the item objects that make a selection based on a received bid. The bidding process is implemented as message passing between agents and items. - 10- The implementation was developed in an incremental fashion . The first implementation was a set of loosely coupled objects that interacted exclusively through message passing. This implementation performed the needed assignment but was slow when compared to solutions written in Fortran and executed on the Sequent. The second implementation of the algorithm was an optimized version of the first. In it we relaxed the interfaces between agents and items, allowing them access to each other's data structures. When these constraints were relaxed, the implementation performed better than the Fortran implementations [ 12]. Throughout the development of this application many of the data structures and concepts brought forth in this paper were used. Many of the object definitions were made in different programs and the reusability of these components has been demonstrated in other work. 4. Conclusions It is described with examples that there are many benefits in the use of an object centered approach to parallel application development, such as the ability to define shared objects with protected members, and the ability to define and enforce interfaces to these objects . Since an object's definition can represent a subtask in the application, when developing concurrent applications, it is useful to define objects as independent processing agents that cooperate in the resolution of the problem to be solved. Vve have shown two classes of concurrent objects that can be implemented using C++. The first was the class of passive data structures that can be shared among several processes, and automatically enforce the needed mutual exclusion and synchronization. We have shown that these data structures can be implemented as abstract data types, and that reusability, implementation biding, and inheritance can be used to ease the burden of development. The second class of objects were those that represented active processing agents which allow the implementation of an application as a set of cooperating subtasks . It is shown that a number of these agents can be created and executed concurrently to provide a speed up in ex er - 13- 53 : return 0; 54: } 55 : } 56 : 57: void sharedStack.error( char *text) 58: { 59: 60: cerr < < form( "Stack error id: reason : %; \n", text); cerr . .flush(); 61 : 62: } 63 : exit( 1); 64: main() 65 : { 66: void funcl(sharedStack*); 6 7: sharedStack iss; 68 : 69 : for (inti= 0; i < 10; i++) iss .push(4); 70: m_set_procs( 4); I I Set the number of child processes to fork 71: mJork(funcl , &iss); II Fork and call funcl( stack* ) 72 : exit(O); 73: } 74: 75 : void funcl(sharedStack *iss) 76 : { 77 : 78: 79 : stackltem i; int myld= m_next(); I I Returns an int that identifies the process 80: while ( i= iss- > pop()) { 81 : i= il 2; 82: if(i!=1){ 83: iss- > push(i); 84: iss-> push( i); 85 : } 86: else 87: cout << form("Number: o/a:l Finished\n", myld ); 88: } 89: } 1: I* 2: process.c 3: Example of process agent object 4: *I 5: #include 6: 7: const int childProc= 0; 8: 9: I I A process object 10: class process { 11: public: - 14- Appendix B 12: int forkReturn; I I forkReturn == 0 if child process 13: process() { forkReturn= fork(); } 14: }; 15: 16: I I A Agent object derived from class process 17 : class derived! : process { 18: public: 19: derived!(); 20: void work(); 21: }; 22: 23 : derivedl.derivedl() 24: { 25: if (forkReturn == childProc) work(); I I Return if parent process 26: } 27 : 28 : I I Loop performing pusdo work 29 : void derivedl.work() 30: { 31 : while (1) {sleep(l); cout <; } 16: void unLock() { s_unlock( &lockP); } 17: }; 18: 19: typedef int stackltem; 20: 21 : class sharedStack { 22 : stackltem array[stackLimit]; 23 : int top; 24: lock myLock; I I Declare a local instance of class Lock 25 : void error( char*); 26: public: 27: I I Class uses shared memory storage allocation 28 : sharedStack() { this= ( sharedStack*) shmalloc( sizeof( sharedStack) ); 29: top= -1; } 30: -sharedStack() { shfree(this); this= 0;} 31 : stackltem pop(); 3 2: void push( stacklte m); 33: }; 34: 35: void sharedStack.push( stackltem item) 36: { 37 : 38: 39: 40: 41:} 42: myLock.setLock(); I I Set the object's lock if ( + +top > = stackLimit) error( "Stack Overflow "); array[ top]= item; myLock.unLock(); I I Un-set the object's lock 43: stackltem sharedStack.pop() 44: { 45 : 46 : 47: 48: myLock.setLock(); if (top>= 0) { stackltem i= array[top--]; myLock.unLock(); 49 : return i; 50: } 51: else { 52: myLock.unLock(); http://uab.contentdm.oclc.org/cdm/ref/collection/uab_ece/id/59