Lemur zaprasza
Teach Yourself CORBA In 14 Days Day 10 Learning About CORBA Design Issues IDL Creep Single-Threaded Applications Server Applications Client Applications Mixed Server/Client Applications Object Lifetime Lack of Pass-by-Value Semantics Rogue Wave ORBstreams.h++ Using CORBA structs Using Conversion Constructors CORBA and X Window System Single-Threaded Applications Using CORBA and X Multithreaded Applications Using CORBA and X Summary Q&A Workshop Quiz By now you have probably determined for yourself that CORBA is a complex architecture. Like any complex architecture, CORBA comes with its own set of issues that affect the design and implementation of CORBA systems. For example, the fact that all interfaces in CORBA are specified in IDL has an effect on developers who want to integrate existing systems with CORBA--usually, IDL interfaces have to be written for a number of existing classes (not a trivial undertaking). This chapter introduces you to such design issues and offers suggestions on how to deal with them. IDL Creep The term IDL creep does not refer to that guy in the office down the hall who thinks he knows everything about the Interface Definition Language. Rather, it is a term coined to refer to the tendency for IDL to permeate a system design--and permeate it does. Think about it: For a class to be understood by CORBA, its interface must be expressed in IDL. Furthermore, if the classes referenced or otherwise used by that class also need to be accessible to CORBA components, then those classes must also have their interfaces expressed in IDL. Consequently, it is not uncommon for most, if not all, classes in a system to require IDL specifications. If you're designing an application from the ground up, having to define IDL interfaces for most (or even all) classes isn't terribly demanding. However, if you're converting an existing application to CORBA, the process can be an arduous one indeed. Because the existing classes were probably not written with CORBA--or a distributed architecture of any kind--in mind, the interfaces for those classes might have to be modified somewhat to mesh well with IDL. Although the modifications themselves might not require a great deal of effort, remember that if the interface of an object changes, any code that uses that object might have to be modified as well. This cascade effect can easily turn a few interface changes in a relatively few classes into a frustrating mess of changes throughout an application. This scenario doesn't even take into consideration the possibility that the non-CORBA application's very architecture might not be amenable to the CORBA architecture. In particular, CORBA's current lack of the capability to pass objects by value can especially affect the design of an application (discussed later in this chapter). Sometimes, modifying an existing application to use CORBA can be like trying to fit a square peg into a round hole. The moral is that IDL is pervasive--it has a way of creeping into a system design and slowly taking it over. This is not necessarily a bad thing in itself, but it can potentially make the "CORBAtization" of legacy applications a difficult prospect. Be prepared to write IDL for any class in an application that might need to be shared between application components. It is not always possible to avoid introducing IDL into most classes of an existing application without entailing a significant redesign of portions (or all!) of the application. However, when designing an application from the ground up, there is one guideline in particular for minimizing the impact of IDL on the rest of the application: Pay close attention to which classes will likely need to be shared between application components and which will not. The underlying classes (e.g., classes that aren't part of any interfaces between components) generally will not require IDL interfaces. Single-Threaded Applications Although most modern operating systems support multithreaded applications--that is, applications in which multiple threads of control might be executing in a single program at the same time--the use of multithreading is still limited. There are several reasons for this: Not all operating systems fully support threading, not all developers use the later versions of operating systems, which do support threading, and not all developers feel comfortable with it anyway. In addition, using multiple threads in applications introduces new issues--not the least of which is the need to manage concurrent access to objects--that complicate application design and development. Consequently, the use of multithreading is not as widespread as it could be. CORBA does not force developers into a multithreaded development paradigm; indeed, it is perfectly feasible to create single-threaded CORBA applications. However, due to the nature of the operation of distributed applications, great care must be taken when designing and developing CORBA applications for a single-threaded environment. This chapter tells you why, covering issues on both the server and client ends of the application. Server Applications The justification for using multiple threads in server applications is simple: A CORBA server might have multiple clients accessing it at any given time. If the server is single-threaded, it can only process a request from one client at a time--if another client attempts to access the server while the server is busy processing a second client's request, the first client must wait until the server is finished (see Figure 10.1). The obvious disadvantage to this architecture is that if the server performs transactions that take time to complete, the apparent responsiveness of the system (as far as the end users are concerned) suffers. Figure 10.1. Single-threaded server operation. One approach to mitigating this problem is to employ the use of multiple servers in an application. Normally, an enterprise-scale application employs multiple servers anyway, for reasons such as load balancing and redundancy. However, it is simply not practical to provide a server per concurrent client, given the amount of overhead required by each server application. It is much more efficient for a single server to handle multiple simultaneous clients, and this is precisely the capability afforded by a multithreaded architecture. In a multithreaded server architecture (see Figure 10.2), rather than process only one client request at a time, the server can start a new thread of execution for each transaction. Because there is always a thread listening for new requests, other clients no longer need to wait for the server to complete a transaction before it can accept the next one. The result is that the server appears more responsive because it can respond immediately to incoming requests. Figure 10.2. Multithreaded server operation. Although the multithreaded architecture can create the illusion of better server performance by enhancing server responsiveness, the architecture does not magically make servers faster. If the server is processing several transactions simultaneously, the speed at which each transaction is processed is likely to decrease relative to a server processing only one transaction at a time. (Multithreaded applications can be more efficient than single-threaded applications, however, so a server processing only one transaction at a time might not be twice as fast as a server processing two transactions simultaneously.) So, although the use of multithreading is not intended to replace the use of multiple servers, it is usually preferable to deploy multiple multithreaded servers than multiple single-threaded servers. Furthermore, in most cases, fewer multithreaded servers are required to deliver the same end user responsiveness. Because multithreading does not come for free, it is important to understand when it is appropriate. Managing multiple threads, especially when it is necessary (as it often is) to prevent multiple threads from simultaneously accessing the same data, can result in costly overhead. Consequently, on a machine with a single CPU, adding multiple threads to an application that is already CPU-bound will only make it slower (although response time to individual clients, as discussed previously, might still improve). On the other hand, on a multiprocessing machine, because each thread can potentially run on its own CPU, performance when using multithreading might increase, even on a CPU-bound application. Where multiprocessing truly shines, however, is in I/O-bound server applications or in applications that act as clients and servers simultaneously (as you will see in the next section). In an I/O-bound server application with multiple client connections, the likelihood that a given thread will be blocked while waiting for I/O increases. Hence, other threads will be given the opportunity to do useful work, thus using the available CPU(s) more efficiently. The bottom line regarding the use of threads in CORBA servers is this: If the server can process transactions quickly enough so that response time is not a concern, a single-threaded server will probably suffice. If the response time of a single-threaded server is not adequate, the use of multithreading is probably a better alternative. Note that this is true only if the server does not also act as a client. If it performs both roles, there might be additional issues involved, as you will soon see. Client Applications Unlike a server application, a client application does not need to concern itself with providing a reasonable response time to other clients. In most cases, when a client calls a remote method on a server, it is perfectly reasonable for the client to wait for the server's response before continuing. This is true as long as one assumption holds true--that the client application is a pure client; that is, the application does not create any CORBA objects and pass references to those objects to other applications. Mixed Server/Client Applications The guidelines for the use of threads in pure servers (applications that behave as servers only, never as clients) are clear-cut: If the response time requirements warrant it, multithreading is preferred; otherwise, single threading is adequate. The guidelines for pure clients are simple as well: Under most circumstances, multithreading is not required. Therefore, you can conclude that single-threaded architectures are adequate for most CORBA applications. If your applications were all pure servers and clients, you'd be right. When an application demonstrates behaviors of a server and a client--call it a mixed server/client application for lack of a better term--the design issues associated with using a single-threaded architecture become insidious. Illustrated in Figure 10.3 is the basic problem: If a single-threaded, mixed server/client application passes one of its objects to a second application in a remote method call, and the second application, in turn, tries to access the object passed to it, both applications become blocked. Figure 10.3. Single-threaded mixed server/client application operation. Of course, this problem is neatly solved by the use of multithreading in the mixed server/client application, as illustrated in Figure 10.4. If multithreading is not an option for whatever reason, another solution is required. The remainder of this section discusses potential solutions to the problems involved in developing single-threaded applications that need to assume the roles of both client and server. Presented here are two useful design patterns for designing CORBA applications using only single-threaded components. Again, the use of multithreading is probably the cleanest method of implementing CORBA applications, but when multithreading is not an option, you will want to consider one of these design patterns. Object Factory Pattern Recall that single-threaded applications that are pure servers or pure clients don't suffer from the deadlock problem illustrated in Figure 10.3. The Object Factory pattern capitalizes on this property of pure client and pure server applications by moving functionality to the appropriate component of the application, resulting in application components that are either pure clients or pure servers. Note:In the Object Factory design pattern, a factory object is responsible for creating objects of a particular type. For example, the Bank object can be thought of as an Account factory, since Banks are responsible for creating all Account objects in the system. Figure 10.4. Multithreaded mixed server/client application operation. To understand how this is feasible, consider the following scenario: A client wishes to call a remote method on a server. The method takes a particular type of object as a parameter; call it a Widget. Now assume that the Widget is created by the client. If the client were to pass a Widget that it created to the server through a remote method, and that method attempted to call a method on the Widget, the result would be a deadlock scenario, as described in Figure 10.3. These are the typical application semantics over which single-threaded applications stumble. It would be most useful if these types of semantics could be achieved in a way that worked with single-threaded applications. This is where the Object Factory pattern steps in (see Figure 10.5). This pattern takes the place of the object creation step. Rather than create the Widget itself, the client requests the Factory (which is the same object as the server whose method the client wishes to invoke) to create the Widget on the client's behalf. The client can then manipulate the Widget, if desired, and can finally invoke the desired remote method on the server, passing the Widget as a parameter. Now, because the Widget exists in the same address space as the rest of the server, the server can manipulate the Widget as it sees fit. Figure 10.5. Object Factory pattern. The Object Factory pattern boasts the advantage of enabling single-threaded clients to pass objects as parameters to remote methods. This capability comes at a price: The server must provide methods for creating every type of CORBA object that a client might want to create. Although providing implementations for these methods is simple, it can be tedious, particularly in applications containing large numbers of classes. Also, it can be more inconvenient for clients to call additional methods to create objects rather than to directly use constructors, although this inconvenience might be minimal. These drawbacks, however, are often outweighed by the advantages offered by the Object Factory pattern. Exclusive oneway Call Pattern Another approach to creating harmony between single-threaded applications and CORBA is what you might call the Exclusive oneway Call pattern. This pattern calls for the exclusive use of oneway invocations throughout the application. Because oneway methods don't block, the deadlock issue associated with single-threaded CORBA applications is eliminated. However, this advantage comes at a price, as you'll soon find out. Note:The Exclusive oneway Call design pattern calls for the exclusive use of oneway methods for communication between CORBA objects. Note:For a review of CORBA oneway methods, refer to Day 3, "Mastering the Interface Definition Language (IDL)." The exclusive use of oneway method calls throughout a CORBA application exacts a potentially stiff penalty: First, the concept of a clearly laid-out program flow is lost. Consider a typical client application. There is generally a well-defined flow of control through the program. That is, there is a main method--it might very well be C/C++/Java's main()--at which the program begins and proceeds to call other methods. Usually, the flow of the program can be determined by tracing the sequence of method calls. In other words, the behavior of the application is at least somewhat predictable because the execution flow can be traced relatively easily. Using oneway methods exclusively, however, radically alters the landscape of an application, as illustrated in Figure 10.6. Instead of a well-defined flow of control traceable throughout the application, the client features a series of seemingly disjointed oneway methods. The exchange between client and server, rather than following the familiar pattern of "client calls server/server returns result" is transformed into a volley of oneway calls from one application to the other. The client starts by calling a method on the server. That method, when completed, calls a method on the client that performs the second step in the application. That method, in turn, calls another method on the server, which eventually calls a method on the client, and so on. Again, because each method called is oneway, there is no blocking due to the client or server being busy. Consequently, each application is free to pass any CORBA objects--including objects created by itself--without fear of blocking. The downside of this architecture is that because the flow of control is now shared between two applications, it is much more difficult to trace. Another penalty that must be paid by developers wanting to use the Exclusive oneway Call pattern stems from a characteristic of oneway methods which you should recall--namely, that oneway messages are not guaranteed to be delivered to their destination. Building an entire application based on this mechanism is certain to be a trying experience, primarily because if there are any methods in the application which require reliable delivery, the developer must implement a mechanism to determine whether a oneway method call actually executed successfully. In other words, the developer essentially must implement reliable delivery semantics on top of the unreliable oneway mechanism, in addition to implementing all the usual application functionality. Figure 10.6. Exclusive oneway Call pattern. To summarize the Exclusive oneway Call design pattern, it should be stressed that this pattern would be extremely difficult to implement for most real-world production systems. Consequently, developers are strongly encouraged to pursue a different method of marrying client and server functionality in a single-threaded application if at all possible. Object Lifetime In a non-distributed application, management of object lifetime is a non-issue: When the application is finished using an object, it simply destroys it. With garbage-collected languages like Java, the developer does not even need to write code to do this--the application handles it automatically. If the application crashes, the memory used by its objects is reclaimed by the operating system. Because all objects are self-contained in the application, there are no issues associated with object lifetime. In a distributed application, circumstances are different. An application might use objects that are local to the application--that is, objects that live in the same address space as the application--or it might use remote objects, which might reside in a different process on the same machine or on a different machine somewhere on the network. Many CORBA ORB products use reference counting for managing object lifetime--a mechanism which works well when applications behave normally. Although the use of a reference-counting mechanism is not dictated by CORBA, its inclusion into some major ORB products merits some discussion here. Recall that in such a reference-counting mechanism, object references are duplicated--that is, their reference count is incremented--when the reference is passed to an application. Similarly, when an application is finished using an object, it releases the object reference, that is, decrements the reference count. When the reference count reaches zero, the object is no longer in use and can safely be destroyed. But what happens when an application holding an object reference crashes before it can release that object reference? This is where the reference-counting mechanism begins to break down. Under these circumstances, the object's reference count never reaches zero, and thus the object is never destroyed. This can be thought of as a sort of memory leak in a distributed application. Consequently, developers might need to think beyond the reference counting mechanism and consider a contingency mechanism that picks up where reference counting leaves off. Because a distributed application cannot truly know when all other applications are finished using its objects (because it doesn't know whether any of those applications have crashed), a mechanism over and above the basic reference-counting mechanism is required. Such a mechanism manages the lifetime of CORBA objects, automatically determining whether an object is in use and, if not, destroying that object. But how does this mechanism determine whether an object is still in use? Actually, it can only guess. To understand how such a mechanism works, consider the following case study. One such mechanism, known as the Evictor, manages object lifetime by tracking the usage of each CORBA object within a server (each CORBA server would contain its own Evictor). When an object has not been in use for a predetermined period of time (for example, a day), the Evictor evicts that object. Although it's possible for the eviction process to simply destroy the unused object, recall that the Evictor does not know for certain that the object is no longer in use. It is possible that one of the clients using that object could be dormant for a period of time, and if the object were destroyed, the client, on waking up, would no longer be able to access that object. To accommodate this possibility, the Evictor does not actually destroy a CORBA object after a period of non-use but evicts it into a persistent store, such as a database. If that object is needed later, it can be resurrected from the persistent store. All this occurs transparently to clients, which are completely unaware that objects are being evicted and resurrected as necessary. Of course, a mechanism such as the Evictor, rather than enabling potentially unused objects to consume memory in a server process's address space, simply moves the unused objects to some form of persistent storage. The result is that unused objects still exist somewhere--in this case, the persistent storage rather than the server application address space. Using persistent storage for this purpose, in addition to offering the advantage of freeing up a server machine's memory resources, provides for simpler maintenance. That is, the objects in persistent storage can be cleaned up periodically, perhaps as a part of the system's periodic maintenance cycle. Purging the database of unused objects in this way would not require system administrators to bring down the server, thus enhancing the server availability. Lack of Pass-by-Value Semantics Perhaps one of the trickiest issues associated with CORBA development is that CORBA does not currently support the capability to pass objects by value. (This limitation is expected to be removed in a later version of CORBA; see Appendix C, "What Lies Ahead? The Future of CORBA," for more details on this and other future enhancements to the CORBA architecture.) Sometimes it is far more efficient for a server to return an object by value to a client so that the client can act on the object locally rather than call a series of remote methods--each of which incurs the overhead of executing a method remotely--on the object. See Figure 10.7 for an illustration of this scenario. Clearly, if a client is to invoke a large number of methods on an object, it is preferable, in terms of efficiency, to act on a local copy of the object rather than a remote one. This is particularly true if the parameters or return value of the method(s) are complex values. Such values are even more expensive to transmit across the network, as is the case with remote methods. If passing objects by value is sometimes a good idea, but CORBA doesn't offer the capability, then isn't the entire discussion a moot point anyway? As it turns out, even though CORBA doesn't offer this capability directly, there are several approaches that emulate this behavior. Rogue Wave ORBstreams.h++ Rogue Wave Software offers a product that enables CORBA applications to pass C++ objects by value. The product, which builds on Rogue Wave's Tools.h++ product, provides the capability to pass many of the Tools.h++ classes by value, as well as user-defined classes that derive from certain Tools.h++ classes or conform to the proper Rogue Wave-supplied interfaces. Figure 10.7. Pass-by-reference versus pass-by-value. ORBstreams.h++ does have some disadvantages. For one, it only supports C++. (If you're implementing a CORBA application entirely in C++--which isn't altogether unlikely--this probably isn't of concern to you.) Also, the product currently supports only one ORB--IONA Technologies' Orbix--further limiting your choice of development tools. Finally, because ORBstreams.h++ builds on the Tools.h++ product, you might be saddled with additional baggage associated with using Tools.h++, if you didn't originally plan on using that product. For all its drawbacks, however, ORBstreams.h++ is a good stopgap solution to the current lack of pass-by-value capability in CORBA. Using CORBA structs Another approach to achieving a pass-by-value-like behavior is the strategic use of CORBA structs. For each CORBA interface that needs to be passed by value, the developer creates a struct that contains members corresponding to all the data members of the class implementing the interface. After these structs are defined, the mechanism works something like this: 1. A method that ordinarily uses interface types for parameters and return value instead uses the corresponding struct types. 2. Before calling such a method, the client creates struct versions of the objects it wants to pass by value. It then invokes the method with these parameters. 3. The server creates objects corresponding to the structs (if necessary), performs its processing, and creates structs for any output parameters or return values that are to be passed by value. 4. The client receives the output parameters and/or return value from the server and, if necessary, creates objects that correspond to the struct parameters. There are a few disadvantages to this approach. The most significant is that inheritance is not a feature of structs; therefore, polymorphism is not supported. In other words, if a particular method takes a certain kind of struct as a parameter, it is not possible to pass another type of struct in its place. Referring to the Bank example, this is akin to creating a CheckingAccount object and passing it back in the place of an Account parameter. However, the CORBA any type might be of use here, at the expense of increased complexity. Using Conversion Constructors A third approach that emulates pass-by-value capability in CORBA is to pass an object normally, but then for the client (or server, depending on whether the object is an input or output parameter) to copy the object's state immediately on receipt. The process is as follows: 1. A client calls a method on a server, which returns an object reference. The server simply returns the object reference as usual. 2. On receiving the object reference, the client creates a new object of the same type, copying the remote object's state into the local one. This is generally done through the use of a constructor that takes the remote object as an argument, converting that object into a local one (hence the term conversion constructor.) 3. The client releases the remote object and continues working with the local one. Compared to the struct approach, the conversion constructor approach has the advantage of being able to work with objects of inherited class types. Additionally, this approach does not require the development of separate IDL interfaces and structs--it is possible to use the exact same implementation classes for local and remote objects. One potential disadvantage to this mechanism is that the local object must call a number of methods on the remote object to obtain its initial state (often preferential to making a number of remote calls over the life of the object). Furthermore, this approach requires that for an object to be passed by value, its interface must provide methods that enable its entire state to be read by another object. This requirement goes against the concept of encapsulation, one of the goals of object-oriented design. It might also require the developer to write more code. CORBA and X Window System One last issue involves the use of CORBA with applications written for the X Window System. In both single-threaded and multithreaded applications, using CORBA and X raises a number of concerns. Single-Threaded Applications Using CORBA and X The primary issue in writing single-threaded applications that use CORBA and X is that both these products try to install what is known as an event loop. An event loop is what the name suggests: a loop in the application code (actually in the windowing system code which is linked with the application code, in the case of X) that waits for an event, processes it, and loops back to wait for another event, and so on, ad nauseum. Such an event loop exists for X as well as for CORBA. In the case of X, the event loop receives and processes events from the X server; in the case of CORBA, the event loop processes events from other CORBA applications. In either case, the event loop is the main loop of the application, designed such that it expects to be running in its own thread all the time. Therein lies the problem with single-threaded applications: Both CORBA and X expect to use their own event loops, each of which expects to be run in its own thread, but there is only one thread in the application. Fortunately, ORB products usually have a mechanism for integrating the CORBA event loop with an X event loop. In these cases, the CORBA events are registered with the X event loop, so the single-event loop can handle events for both products. You can refer to your product's documentation for more information on how this is accomplished. Multithreaded Applications Using CORBA and X In a multithreaded environment, it is perfectly viable to run separate event loops for X and CORBA, so the issues applying to single-threaded applications don't apply to multithreaded applications. However, there are a couple of issues to be aware of: Older revisions of X--versions prior to X11R6.1--are not thread-safe and therefore must be used with care in a multithreading environment. This means that the developer must take additional steps to ensure that multiple threads don't access X library calls at the same time. (As of X11R6.1, however, X is thread-safe and does not suffer from this restriction.) A related issue is Motif, a common user interface library for X. As of the time of this writing, there is not yet a thread-safe version of the Motif library. Thus, even with the thread-safe X11R6.1 or greater, developers still need to take care that multiple threads don't execute Motif library calls at the same time. As a result, integrating CORBA with a Motif application in a multithreaded environment, at least with the current version of Motif, takes as much effort as integrating CORBA with a non-thread-safe X library in a multithreaded environment. Integrating multithreading, CORBA, and non-thread-safe X and/or Motif is certainly possible, although you can expect it to take some work. All non-thread-safe calls need to be wrapped in methods that ensure that only one thread can call such a method at any given time. One way of ensuring this is through a thread queue, a mechanism enabling multiple threads to be queued (in other words, wait in line) for access to non-thread-safe code. As thread-safe versions of X and Motif proliferate, this will become less of an issue, but for now, CORBA developers should be aware. Summary Today you examined several issues associated with developing CORBA applications. The most significant are those associated with developing CORBA applications in a single-threaded environment and those raised by CORBA's current lack of pass-by-value capability. You also learned a few, and by no means an exhaustive list of, workarounds for these issues. On Day 11, you'll move on to the next topic in advanced CORBA development: use of the Dynamic Invocation Interface (DII). The DII enables CORBA applications to learn about each other dynamically (in other words, at runtime) and access newly discovered services. Q&A Q If non-trivial single-threaded CORBA applications raise so many design issues, why wouldn't someone just use multithreading? A Although multithreading is often the preferable alternative to wrestling with the issues raised by single-threaded applications, there are times when multithreading simply isn't available, such as when single-threading is dictated by choices of other applications or development tools. It is for cases such as these that the design patterns dealing with single-threaded applications are intended. Q It was mentioned earlier in the chapter that some CORBA products implement reference counting to manage object lifetime. What other way can this be accomplished? A Another mechanism that can be used to manage object lifetime is for each remote object to have a heartbeat. Other objects, or the ORBs themselves, can ping each remote object to determine whether that object is still alive. If an object doesn't respond to the ping within a preset period of time, the other object can assume that the application containing that object has crashed. (As it turns out, a mechanism similar to this one is used by other ORB-like products such as Microsoft's DCOM and ObjectSpace's Voyager.) Workshop The following section will help you test your comprehension of the material presented today and put what you've learned into practice. You'll find the answers to the quiz in Appendix A. On most days, a few exercises will accompany the quiz; today, because no real "working knowledge" material was presented, there are no exercises. Quiz 1. What is the major issue associated with mixing client and server functionality in a single-threaded CORBA application? 2. How can the use of reference counting in a CORBA application lead to problems? 3. Which version of X11 (the X Window System) would be required to safely run multithreaded X-based applications? 4. Why is the capability to pass objects by value sometimes useful? 5. Why is it usually inadvisable to use the Exclusive oneway Call design pattern introduced earlier in this chapter? © Copyright, Macmillan Computer Publishing. All rights reserved. |