Recommended ResourcesThe following websites are relevant to or provide additional information on the topics discussed in this chapter. Websites for competing technologies to ones discussed in this chapter:
Alternative technology websites:
|
Sunday, October 18, 2009
Recommended Resources
9.2 A Simple Database
|
9.2 A Simple DatabaseAll of the examples in this chapter were tested with the MySQL database management system (DBMS).[3] "Setting Up the MySQL Database Used in This Book" on page 556 provides instructions on downloading and installing MySQL and an associated JDBC driver, in addition to showing you how to create and populate the database used in this chapter.
The examples in this chapter access a simple database with three tables: Customers, Orders, and Accounts. Figure 9-1 shows the data contained in the Customers table. Figure 9-1. The Customers TableInitially, the database contains 20 customers. Each customer has a customer ID, name, phone number, and address. In the interest of simplicity, all of the (fictitious) customers reside in North America. Figure 9-2 shows the Orders table and the Accounts table. Figure 9-2. The Orders Table (left) and the Accounts Table (right)Each order in the database contains an order number, an order date, a customer ID, an amount, and a description. For the sake of brevity, only the first seven customers in the database have orders. All of the customers have accounts. The Accounts table is only used by the example in "Executing Database Transactions" on page 411. For the sake of illustration, the tables shown in Figure 9-1 and Figure 9-2 use a simplified design; for example, the Orders table keeps track of orders for single items with no notion of quantity and directly stores order amounts and product names that are localized for English. A better design would include an additional Products table that listed product IDs and prices and a modified Orders table that stored product IDs instead of descriptions and quantities instead of amounts. Moreover, you should never store localized data in a database, because you will want to easily localize that data when you display it. In general, you should adhere to the following rules when storing data in a database:
Store prices in a locale-independent manner in a fixed representation, such as the format defined by the java.lang.Long.toString or java.lang.Double. toString methods. The Orders and Accounts tables in Figure 9-2 store their currencies in that manner. Note that the examples in this chapter are not internationalized. The database design and the lack of internationalization in this chapter are intentional because proper database design and internationalization would obfuscate the concepts that this chapter illustrates, namely, how to use the JSTL SQL actions. |
|
21.3 Code Introduction
| |||||||||||||||
Chapter 16. File Input/Output
I l@ve RuBoard |
Chapter 16. File Input/Output
A file is a collection This chapter discusses three different I/O packages. The first is the |
I l@ve RuBoard |
Data Access Technology Survey
3 4
Data Access Technology Survey
The field of data access is large and confusing. So much so that I think it is best if we start with a map to orient ourselves. Table 14-1 lists these properties of many (though by no means all) common data access technologies:
- COM+ support. A technology is said to support COM+ if it manufactures resources through a Resource Dispenser. In the case of transactional resources, such as RDBMS connections, an RM must be provided and the Resource Dispenser must perform automatic transaction enlistment.
- Provider-specific API. A technology is said to be provider-specific if it does not support data access through an open standard but instead allows access to only a restricted set of data sources through a proprietary interface. ODBC-based open technologies allow access to data that is backed by some type of SQL query engine. OLE DB-based technologies remove even that restriction and allow access to a much larger variety of data sources.
- Programming model level. This is the level of abstraction at which the technology offers data access and manipulation to clients. Factors such as exposure of raw handle types, existence of a navigable object model, and exposure of memory buffers play a role in whether an access technology qualifies as high level.
- Interface type. An access technology can be accessible only by a certain programming language, or it might offer COM+ interfaces.
- COM+ support anchor. Many high-level data access technologies are layered on top of low-level ones. This column lists the level in the hierarchy at which the COM+ support is provided.
Table 14-1 Database Interfaces and Their Fundamental COM+-Related Properties
API or Object Model | COM+ Support | Provider- Specific API | Programming Model Level | COM+ Interface Type | Support Anchor |
---|---|---|---|---|---|
ODBC | Yes | No | Very low | C | Native |
OLE DB | Yes | No (and not SQL specific) | Low | COM+, with pointers | Native |
RDO | Yes | No | High | COM+ | ODBC |
DAO with ODBC- Direct | Yes | No | High | COM+ | RDO and thus ODBC) |
DAO with Jet work- space | No | Jet-connected and installable ISAM data sources | High | COM+ | N/A |
ADO | Yes | No (and not SQL specific) | High | COM+ (including Java through WFC) | OLE DB |
JDBC (via JDBC- ODBC bridge) | Yes | No | High | Java | ODBC |
ATL | Yes | No (and not SQL specific) | High | C++ | OLE DB |
MFC | Yes | No | High | C++ | ODBC |
ESQL | No | No | Query language | Language extension through precompilation | N/A |
Microsoft SQL Server DB-Library | No | Yes | Very low | C | N/A |
Oracle Pro*C | No | Yes | Query language | C language extension through precompilation | N/A |
OCI | No | Yes | Very low | C | N/A |
OO4O | Yes | Yes | High | COM+ | Native |
Support that is not provider specific for COM+ is rooted in two technologies: ODBC and OLE DB. I will discuss OLE DB in the next section. ODBC is an older standard for interoperation with relational data stores. I do not know of an RDBMS out there today that does not support ODBC access. When compared to OLE DB, the large number of ODBC-compliant RDBMSs is the advantage of ODBC. However, there is an adapter (referred to as a provider in OLE DB lingo) that converts an ODBC data source to an OLE DB data source. Clients written to the OLE DB specification therefore can take advantage of more data sources than those written to the ODBC specification, at least on Microsoft Windows platforms.
ODBC's driver manager is a Resource Dispenser as of version 3.0 of ODBC. This means that connections to ODBC data sources are pooled and automatically enlisted in transactions if you have version 3 or later of ODBC installed. In order for this to work, some cooperation also is needed from the ODBC driver you are using. In particular, the driver must support the attribute SQL_ATTR_ENLIST_IN_DTC in the function SQLSetConnectionAttr of the driver API.
While the ODBC interface is too low level to allow for productive development and maintenance of modern applications, its support for COM+ is still crucial. There is a lot of source code out there that accesses the data store either directly through ODBC or through a technology layered on top of ODBC. This code exists in various forms, including C function libraries, C++ object hierarchies, and unconfigured legacy COM objects. Because ODBC now offers a Resource Dispenser, all this software can be reused without change and can benefit from transaction enlistment and connection pooling. All you have to do is invoke such legacy software in the context of a transactional COM+ object and voila!—your performance might increase, and the work done by your legacy code will be part of the distributed transaction umbrella with ACID properties orchestrated by your COM+ object.
Technologies layered on top of ODBC include Data Access Objects (DAO), but only when used with ODBC-Direct; Remote Data Objects (RDO); Java Database Connectivity (JDBC); and the MFC classes CDatabase and CRecordset. MFC also offers a set of separate wrapper classes of DAO for C++. Among these second-tier ODBC encapsulation technologies, DAO is the oldest. Microsoft built it originally as a high-level interface for COM clients such as Microsoft Visual Basic to allow access to its Jet desktop database product. Next came RDO, which is a COM object wrapper library around ODBC. With RDO, clients could reach a greater number of local and remote data sources. ODBC-Direct was invented to make this expanded access available to existing DAO clients. In an ODBC-Direct workspace, DAO becomes just a wrapper of RDO. The two MFC ODBC wrapper classes form a very thin veneer on top of ODBC, benefiting C++ desktop applications. An advantage of the MFC approach is that it allows direct access to the ODBC API through the
wrapper classes. It therefore yields the benefit of elevating the programming model without the loss of flexibility. JDBC remains a strong option even today because it is the only fully portable data access solution for Java. However, when portability is not a concern, the more feature-rich ADO available through the Windows Foundation Classes for Java (WFC) is a better choice.
OLE DB has provided a Resource Dispenser since its inception. Even though the OLE DB interface is COM+ based, it is still not high level enough to make for maintainable application code. In addition, OLE DB COM+ interfaces contain constructs that cannot be consumed by all COM+ client development systems, in particular Visual Basic (see Chapter 5), and require that clients provide memory buffers with instructions to OLE DB on how to align data from providers within them. This data access idiom of data to memory location binding is certainly not new; it has existed in the earliest versions of proprietary, low-level C interfaces to relational databases. But this idiom is too error prone for business logic development in any modern programming language. OLE DB lies at the core of all data access in the development of modern applications on Windows platforms, and it is the level at which COM+ integration is provided by a Resource Dispenser. Clients normally interface with a wrapper technology on top of OLE
DB.
Such wrapper technologies include the ActiveX Data Objects for all COM+ clients and the Active Template Library (ATL) OLE DB consumer templates, exclusively for C++. ADO provides a navigable object model through which many (but not all) OLE DB features can be exploited. ATL offers a set of templates in the tradition of generic programming. ATL operates at roughly the same level of abstraction as ADO but nevertheless offers faster access and a number of other significant benefits that are especially important to C++ clients. ATL also allows unrestricted access to the full set of OLE DB functionality. I will discuss specific C++ data access considerations later, in the "C++ Data Access" section of this chapter.
By far the two most popular database products used in COM+ projects are Oracle's RDBMS and Microsoft's SQL Server. Like SQL Server, Oracle offers a native OLE DB provider with version 8i, release 2.3 Oracle has implemented this provider directly on top of its low-level Oracle Call Interface (OCI). It now supports updateable and scrollable rowsets, Windows-integrated authentication, access to PL/SQL stored procedures, and rowsets returned from both stored procedures and functions, among other features. Both technologies layered on top of OLE DB—ADO and the ATL consumer templates—allow developers to implement straightforward and maintainable code that can access Oracle databases on Windows and other platforms.
In addition to the OLE DB provider, Oracle offers Oracle Objects for OLE (OO4O), a set of COM+ objects whose interfaces can be consumed by any client. OO4O has a native Resource Dispenser and is therefore fully integrated with COM+. There are some trade-offs with using OO4O, however. On the plus side, OO4O provides access to Oracle RDBMS-specific features. Oracle also reports better performance with OO4O than the performance you would achieve when going through layers of OLE DB software. On the minus side, fewer developers are familiar with OO4O than, for example, ADO. Therefore, you might need to invest more in training over time. More significantly, OO4O is proprietary technology offering access to Oracle RDBMS products only, both now and in the future. By using OO4O, you lock yourself into using Oracle software. If you are comfortable with that, OO4O is a good option for COM+ projects.
Like all databases, Oracle also offers lower-level interfaces. These include the C-based Pro*C and OCI. Applications theoretically can be written directly against those interfaces. A number of databases, including SQL Server, support the old Embedded SQL (ESQL) standard, a mechanism that embeds SQL statements directly into the source code of a programming language and uses a preprocessor to make those statements compilable in that language. This is exactly how Oracle's Pro*C works, although Pro*C can be used only in C and C++ source code. SQL Server also supports its own low-level flat API, called DB-Library.
All of these data access interfaces are so low level that COM+ integration cannot be provided. No Resource Dispensers exist at that level, and services such as connection pooling, which theoretically could be implemented independent of a COM+ Resource Dispenser, are not offered either. These APIs serve as the lowest level RDBMS interface on which middle-tier services such as COM+ integration are meant to be layered. You might consider these APIs the assembly language of data access. Assembly language is the interface to a processor on which you layer programming languages. You wouldn't consider implementing your business logic in assembly, would you? I discourage the use of any data access technology that does not provide COM+ support in a COM+ project. Avoid especially the low-level interfaces because they tend to degrade source code maintainability by another level of magnitude over that caused by forcing business logic to shoulder the burden of manual transaction management or ACID behavior
simulation. Also, as with any database interface without COM+ support, they force developers to think about controlling database connections efficiently—implementing manual connection management might require substantial effort.
When integrating legacy code into a COM+ project, you will not have much of a choice to make when it comes to selecting a data access method. If the legacy code is ODBC based, you stand a good chance of being able to integrate it with only moderate effort. If the legacy code is not ODBC based, leveraging the benefits of distributed transactions with ACID properties and heterogeneous participants as well as gaining the advantages of centralized resource pooling could be very difficult. But what about brand-new COM+ code? Given all these options for COM+ integration, which one is best?
I suggest these guidelines: First, recognize that the future of data access on Windows platforms rests firmly on the OLE DB foundation. OLE DB (with the help of the ODBC provider) is by far the most universal and widespread data access interface on Windows today. Expect new types of data sources to appear in the Windows market with OLE DB providers first, and perhaps no other interface at all. Second, make a choice that suits the development environment you use. The following are good combinations:
- Use ADO in Visual Basic components.
- Use ATL OLE DB consumer templates in Visual C++ components. I will discuss conditions for using ADO in C++ and options for doing so later in this chapter, in the "C++ Data Access" section.
- Use ADO through WFC in J++ components. Use JDBC if Java code must be portable.
- Consider OO4O in any development environment for access to those data items that are tied to Oracle products.
Combining Large-Scale Structures and Distillation
[ Team LiB ] |
Combining Large-Scale Structures and DistillationThe concepts of large-scale structure and distillation also complement each other. The large-scale structure can help explain the relationships within the CORE DOMAIN and between GENERIC SUBDOMAINS. Figure 17.6. MODULES of the CORE DOMAIN (in bold) and GENERIC SUBDOMAINS are clarified by the layers.At the same time, the large-scale structure itself may be an important part of the CORE DOMAIN. For example, distinguishing the layering of potential, operations, policy, and decision support distills an insight that is fundamental to the business problem addressed by the software. This insight is especially useful if a project is carved up into many BOUNDED CONTEXTS, so that the model objects of the CORE DOMAIN don't have meaning over much of the project. |
[ Team LiB ] |
4.2 Understanding CVS
< Day Day Up > |
4.2 Understanding CVS
The idea behind CVS, as with any repository software, is to manage
In general, there are two models for source code repositories:
By default, CVS supports optimistic locking�although some CVS Because each file needs to be kept track When you update a file from the repository, your local version of the CVS also supports |
< Day Day Up > |
1.5 Architecture of Microsoft Windows OS
[oR] 1.5 |
< BACK  NEXT > |
Section 1.4. Injecting Dependencies with Spring
1.4. Injecting Dependencies with SpringYou're almost through with the setup for Spring. When I started using Spring instead of J2EE, it changed my life. I 1.4.1. How do I do that?You'll first have to Moving a well-designed plain-ordinary-Java-object
Since our individual parts are already built to take advantage of Example 1-9 shows the configuration file. Example 1-9. RentABike-context.xml<?xml version="1.0" encoding="UTF-8"?> And Example 1-10 is the new assembler that replaced Example 1-10. RentABikeAssembler.javaimport org.springframework.context.support.ClassPathXmlApplicationContext; 1.4.2. What just happened?You may be scratching your head and wondering what the big deal is 1.4.3. What about......Pico, Hive Mind, and Avalon? These are all lightweight containers. |
21.6. Boost Libraries in Technical Report 1 (TR1)
|
Chapter 4. Compilation: The Basics
Chapter 4. Compilation: The BasicsBefore continuing, let's review how computer programs get made. If you're coming to Xcode from long experience with GNU Make or another development environment, this discussion will be extremely familiar to you. Programmers use source code to specify what a program does; source code files contain a notation that, although technical and sometimes cryptic, is recognizably the product of a human, intended in part for humans to read and understand. Even the most precise human communication leaves to allusion and implication things that a computer has to have spelled out. When the Linrg tool refers to the local variable slope, for example we cared only that the name slope should consistently refer to the result of a particular calculation; the central processor of a computer running Linrg, however, cares about the amount of memory allocated to slope, the format by which it is interpreted, how memory is reserved for the use of slope and later released, that the memory should be aligned on the proper address boundary, that no conflicting use be made of that memory, and, finally, precisely how the address of slope is to be determined when data is to be stored or retrieved there. The same issues have to be resolved for each and every named thing in a program. |
Elements of Interception
3 4
Elements of Interception
COM+ performs all its transparent magic through a process known as interception. Calls from clients to objects are intercepted by an entity that implements the same interfaces as the object but adds services before passing the call to the object. It is easy to see how such a technique might regulate concurrency through locking: the interceptor could acquire a critical section before passing on the call. In fact, you might have used such a technique years ago while synchronizing access to a library that was not thread safe. We will examine interception implementation in more detail in a moment. For now, suffice it to say that COM+ always implements concurrency management through some form of interception. Because interception is such a central concept in COM+, we will step outside the bounds of examining concurrency management in this section to take a broader look at how interception is used in COM+ middleware.
Concurrency vs. Reentrancy
Do not confuse an object's tolerance for being accessed by multiple threads—perhaps simultaneously—with its ability to handle calls back to the object by a logical thread that was used to make a call from the object. If object A can handle a call by object B, which object A is currently in the process of calling (either directly or indirectly), object A is said to be reentrant.
It is not the place of object middleware to regulate reentrancy. Only your object design can ensure that your object will not be called back while waiting for an outgoing call to return, in the event it cannot handle such a call. Your design might need to provide this assurance because blocking a reentering call would guarantee deadlock. In fact, COM+ puts some effort into ensuring that callbacks are always serviced and therefore never result in deadlock. This implementation is more challenging than you might think, since a callback can occur on threads other than the one waiting for the return of the method invocation. We will look at this issue more closely when we examine locking (in its own section) later in this chapter.
Interception Implementation
The concept of interception is quite simple: an arbitrary object is wrapped so that some amount of work can be done before and after calling a method in an interface the object supports. The most familiar example of an interceptor is COM's venerable proxy. A proxy (or more precisely, proxy manager) acts like the object it represents, but its job is to marshal all call parameters for transmission through the channel to the stub, which will unmarshal the parameters, reassemble the stack frame, and then make the call to the actual object. Yet the traditional proxy (as opposed to the newer, stubless proxy) is not a perfect example of an interceptor, since it is not generic. The MIDL compiler generates functions that mirror each method of the interface the proxy wraps.
The generality of a true interceptor presents a challenge during implementation. A generic interceptor does not know the shape of the interface it will wrap once it is compiled. This lack of information at compile time creates a very interesting set of difficulties.
The Language Problem
Procedural high-level languages, including C and C++, generally are not capable of calling a function with an unknown parameter list. By using the ellipsis and va_ set of functions, you can implement a function that does not know what parameters it will be called with at compile time. However, you cannot tell the compiler to make a call to a function and simply pass the parameters that were passed to the function making the call.
This problem can be overcome only by using a piece of assembly language to make the call from the interceptor to the wrapped object. Essentially this assembly code must make the call to the target function in the wrapped object while leaving the stack frame unchanged. However, the C compiler will already have altered the stack frame with a standard function prologue segment, which lets you access local variables. Microsoft Visual C++ offers the __declspec(naked) storage class attribute, which will prevent function prologue and epilogue generation. Obviously, implementing naked functions is difficult and, along with the necessary assembly segment, requires a thorough understanding of the processor architecture for which you compile your code.
The Failure Problem
COM+ interface methods always use the __stdcall calling convention. This convention has the callee, not the caller, clean up the stack before returning from the function. This is no problem if you actually can make the call to the object for which you are intercepting, but what if your interception task fails? What if you would rather not make the call under a certain set of circumstances, or what if the object you want to dispatch the call to is unavailable? Now you are responsible for removing parameters from a stack frame whose shape you don't even know.
Of course, there are ways to get around this problem. For example, you might require the caller to tell you the combined size of all parameters. However, this approach is somewhat awkward and makes your interception hardly transparent. Or you might try to derive the combined parameter size by querying the ITypeInfo interface of the target object. Of course, the object might not support this interface, in which case you could attempt to create a stub for the interface you want to wrap, and interpret its CInterfaceStubVtbl structure, defined in RpcProxy.h. And your interceptor must create a stub and interpret the structure before your wrapper function is called, since determining stack frame size inside the wrapper cannot tolerate failure. By now you've probably guessed that doing this will require significant effort.
The Post-Processing Problem
Your interception task might require work before making the call to the wrapped object, as well as afterward. This means that after the wrapped function is complete, it must return to your wrapper rather than that wrapper's caller. Therefore, you need to change the return address on the stack so that it points within the wrapper function. But how do you remember the address of the caller to which you must return after you finish post processing? After all, there is no place on the stack to store this information.
You can save the final return address in thread local storage (TLS). But allocating a new TLS slot can be expensive if interceptor calls nest on the same thread, and you might find yourself running out of slots. Therefore, you should manage a stack of return addresses via a single slot, instead of allocating a new slot for each function invocation.
Make no mistake: implementing generic interception is very challenging and is nonportable. Even if you never need to implement an interceptor in your own software,2 understanding the issues of the task gives you a better grasp of what is happening inside the COM+ middleware, if not sympathy for the developers who created it.
The Apartment
The COM+ apartment model lets objects make a statement regarding their thread affinity. An in-process server makes this statement declaratively by setting the ThreadingModel named value under the InprocServer32 key under the class ID key in the registry, generally at registration time. Before the MTS COM era, the apartment defined an object's innermost execution context—that is, the COM run-time environment would never inject itself between objects that resided in the same apartment. COM+ allows each object to choose from one of the following apartment types:
- The single-threaded apartment (STA). An object created in this apartment is entered only by the unique thread that comprises the apartment. A ThreadingModel value of Apartment indicates that an object requires instantiation within an STA. A user thread can create such an apartment by calling CoInitialize or CoInitializeEx with COINIT_APARTMENTTHREADED. Calls into the apartment are received by the channel via window messages; therefore, each user thread that creates this apartment type must service a message loop until no objects remain in the apartment. Otherwise, calls to objects in the apartment cannot be serviced and will block. Since STA objects can be entered only by their creating thread, no concurrency can exist within them. Microsoft Windows NT and Windows 2000 will place a new STA object in the system STA—unless the caller resides in an STA itself, in which case the new object will be co-located in the caller's apartment. The system STA is an apartment
owned by a thread created by the COM/COM+ library. The library arranges for this thread to service a message loop for the lifetime of the process. At most, one system STA will be created per process. The system STA is the only STA that can be created in a process by COM preceding the MTS era. - The main single-threaded apartment. By omitting the ThreadingModel named value or setting it to Single, an in-process server's object indicates that the object requires instantiation within the unique main STA of a process. This main STA is formed by the first user thread that creates an STA. If no STA exists within a process yet, the system STA will become the main STA.
Legacy in-process servers sometimes use this setting because their objects were written under the assumption that they could share global in-process server data without requiring locking. An ActiveX DLL created by Microsoft Visual Basic also supports this setting.3 The setting is not recommended for new projects because it can lead to contention among all the COM objects that are forced to share the same thread.
- The multithreaded apartment (MTA). Objects with ThreadingModel set to Free are created in this apartment. There is only one such apartment per process, and user threads can join it by calling CoInitializeEx with COINIT_MULTITHREADED. Such threads need not service a message loop and can terminate at any time. Objects in the apartment receive calls on arbitrary threads created by the Remote Procedure Call (RPC) run-time library. This apartment type does not imply synchronization, and objects running under COM prior to MTS as well as unconfigured COM+ objects must prepare for concurrent entry by callers. Visual Basic 6 objects cannot use this setting.
- The thread-neutral apartment (TNA). This apartment type is new in COM+. Its objects are entered directly by the caller's thread, whether it is an STA thread or belongs to the MTA. Threads cannot belong to this apartment; they merely enter it for the duration of a call sequence. Like the MTA, this apartment type does not imply synchronization. Unconfigured COM+ objects must prepare for concurrency. Visual Basic 6 does not support this setting.
An object can also declare ThreadingModel equal to Both, in which case it will be created in the apartment of its caller. The value Both is used for historical reasons: it originated at a time when COM supported only two apartment types. An unconfigured component using this setting might experience concurrency, as its creator might be an MTA thread or a TNA object. The primary motivation for using this setting is to eliminate an apartment boundary between an object and its instantiator.
Table 4-1 illustrates which apartment COM and COM+ will choose for instantiation of a new unconfigured object, given that object's ThreadingModel and the instantiating thread's apartment membership. (Of course, the TNA row and columns are relevant to COM+ only.)
Table 4-1 Instantiation Apartment Selection
Instantiator | Single | Apartment | Free | Neutral | Both |
---|---|---|---|---|---|
Main STA | Main STA | Main STA | MTA | TNA | Main STA |
Secondary STA | Main STA | Caller's STA | MTA | TNA | Caller's STA |
MTA | Main STA | System STA | MTA | TNA | MTA |
On loan to TNA | Main STA | System STA | MTA | TNA | TNA |
Whenever a thread invokes a method on an object across an apartment boundary, the invocation is intercepted by the object's proxy, routed via the channel, and then delegated to the object by the stub on the other side of the channel. The COM+ middleware performs three important functions when intercepting method calls across apartments:
- Since apartment switches that do not involve the TNA imply thread switches, the proxy and stub are responsible for packaging the stack frame and reassembling it on the object's thread.
- Notifying the target apartment about incoming COM+ traffic can involve sending window messages or some other interprocess communication (IPC) mechanism. This notification is the channel's job. Crossing an apartment boundary that necessitates a thread switch imposes significant overhead. A ThreadingModel value of Both will eliminate this overhead between instantiator and object, and the TNA will eliminate the overhead in all cases for subsequent callers from other apartments and for the instantiator.
- Object references from the originating apartment are converted to proxies in the target apartment. This prevents a thread in the target apartment from crossing into the object's apartment without interception. The new proxy is always directly connected to its object's apartment and does not detour through the caller's apartment unless the object resides in the caller's apartment.
Making a call to an object in the TNA within the same process never requires switching to a different thread. Only the last item in the previous list needs to be performed by an interceptor guarding access to the in-process TNA. Such an interceptor is sometimes called a lightweight proxy. Compared to the overhead of a thread switch, a lightweight proxy is very fast. But the lightweight proxy still needs to perform object reference conversion, as shown in the graphic at the top of the next page. For this reason, a TNA interceptor needs access to the proxy/stub DLL of the interface it is encapsulating, or in the case of a type library-marshaled interface, to its type library. Such access is also necessary for the interceptor to handle the failure problem inherent in general interception.
One of the consequences of a user thread's inability to join the TNA is that the thread always incurs the overhead of at least a lightweight proxy when creating or calling an object within the thread-neutral apartment. This is not necessarily true for creating or accessing an object in one of the other apartment types. If a user thread performing a watchdog activity or other periodic task needs frequent access to COM+ objects, placing these objects in the TNA could impede performance. However, such situations are relatively rare and in most architectures are confined to "system" type objects rather than objects containing business logic.
Managing STA Concurrency
Since only a unique thread can enter an object created in a single-threaded apartment, an STA object naturally avoids concurrency. However, method invocations are serialized not only for an individual object in the apartment, but also for all objects created in the apartment, since they all share the same thread. Therefore, it can be important to actively control which STA will be chosen for an object instantiation; otherwise, you might end up with large groups of objects blocking one another from executing, even though such concurrency in separate objects would be perfectly all right. Often the only thing preventing two objects from executing concurrently is their need to access shared global data. Such data access frequently is better controlled by using explicit locking strategies (described later in this chapter), rather than using the somewhat heavy-handed approach COM+ has for invocation serialization. Given that a group of objects needs to reside in an STA, the question becomes how many objects
should share one apartment for optimal concurrency. The pressures to be balanced include each individual object's responsiveness as well as the amount of threads the system can handle before the thread scheduler's overhead becomes too significant.
Under normal circumstances, the instantiator of an object implicitly selects the object's apartment. But it is typical to see a client take control of in-process server concurrency by first creating a single-threaded apartment and then issuing the instantiation call from this new apartment. This approach can be effective in situations where the client process ultimately is aware of how server objects are being used, and how their concurrency can best be exploited.
MTA-Bound Call Interception
Since the introduction of the multithreaded apartment on Windows NT, COM developers frequently have asked why a thread executing within an STA object cannot call directly into the multithreaded apartment. With the addition of the thread-neutral apartment under COM+, the question becomes why a thread that originally created a single-threaded apartment cannot enter the multithreaded apartment, whether executing in its own apartment at the time of the call or on loan to the thread-neutral apartment when the call is dispatched. The question generally acknowledges that you would still need lightweight interception to prevent MTA threads from crossing over into the calling apartment when accessing object references passed as interface method arguments, but such interception should be feasible without incurring the expensive thread switch. After all, MTA objects are written to be entered by arbitrary threads and therefore it doesn't matter whether a calling thread actually belonged to a single-threaded
apartment.The justification for switching threads involves an STA thread's need to service a message loop somewhat frequently, and with the expectation of the MTA object developer. This justification is not so much connected to the mechanism the channel uses when making an outbound call from the MTA: normally this mechanism involves blocking the calling thread until the method invocation returns, but the channel has the power to discover a thread's native apartment membership and can enter a message loop waiting for call return if the calling thread is an STA thread, even when making a call from an MTA object. The real problem is that the MTA programming model allows the object developer to unconditionally block threads and to do so for arbitrary periods of time—usually for synchronization purposes or when waiting to access a resource. Therefore, within MTA object implementations, you frequently will find calls to EnterCriticalSection, WaitForSingleObject, and
WaitForMultipleObjects that have long time-outs. The STA thread must protect itself from running into code like this; it does so by waiting in a message loop at the apartment boundary and having an MTA thread block in the synchronization APIs instead.
In other situations, the server is best equipped for arranging object-to-apartment distribution. This includes cases where server objects do not run in the client's process space, making the client unable to affect target apartment selection. At first it might appear that the server is unable to influence this apartment selection, since COM+ makes all the choices. This impression might have been furthered by my choice of words: to simplify the discussion, I always speak of the apartment in which the object will be created. In fact, the object's class factory—not the object itself—will be instantiated in the apartment by the COM+ library. Therefore, it is up to the class factory to create the actual object. Normally the class factory creates the object inside its own apartment, but it does not have to. The indirection of the class factory in the object creation process gives servers the ability to take control over an STA object's target apartment.
Visual Basic allows developers to multiplex objects, created externally or internally by means other than New, across a thread pool of a fixed size on a per-project basis. Alternatively, developers can specify that each object created in this manner should be located in a new apartment. These options are available to local servers only. Of course, C++ developers implementing their factories manually can do whatever they like to control target apartment selection or creation. But when using the Active Template Library (ATL), you can achieve multiplexing across a pool by using the macro DECLARE_CLASSFACTORY_AUTO_THREAD. By default, the size of the thread pool used by this mechanism will be four times the number of processors of the system on which your code executes. This dynamic way of determining pool size makes more sense than the Visual Basic approach, because a pool that is too large will degrade performance as much as a pool that is too small will cause insufficient object concurrency.
The Context
With MTS, COM started taking on additional services that needed to be handled at the level of the interceptor, including transaction support and role-based security. Such services have expanded under COM+ while becoming more configurable. We will take a survey of these interception services in a moment.
Before the days of MTS, COM interception was married to the concept of the apartment, and apartments are about threads. But uniting the new services with the thread infrastructure did not make sense, so a tighter execution scope for COM objects was needed. This new innermost execution scope is called the context. An object now resides within a context, and an apartment bounds a context. Extended COM+ run-time services are performed by interceptors, which must exist between any two objects that do not reside in the same context. As long as the interception occurs between contexts in the same apartment or on a TNA-bound call, these interceptors generally are as efficient as any lightweight proxy that does not require a thread switch. But recall that the interceptor still needs access to your proxy/stub DLL or type library, for the same reason a lightweight proxy requires this access. Two objects with similar configurations and the same interception needs may share the same context, but certain services require that an object be created in an entirely new context. Threading model setting permitting, an unconfigured object, which, by its very nature does not ask for and is unaware of extended services, is always co-located in its instantiator's context.
Of particular interest is a COM+ service specifically dedicated to managing concurrency in an object. I have to admit that I was initially quite confused by this service. Having worked with the apartment model for years, the concepts of thread affinity and synchronization had become indistinguishable in my mind. But upon later reflection, I realized that while a relationship between the concepts exists, the concepts for the most part are independent. There is no reason, after all, to not serialize access to either a multithreaded or a thread-neutral object. In Essential COM (Addison-Wesley, 1998), Don Box speculated about a new apartment type he called the rental-threaded apartment (RTA). This apartment type would have behaved just like the thread-neutral apartment of COM+, but with synchronization built in. This is just the type of idea that was bound to emerge from a mindset that identifies apartments with concurrency management. Yet the decoupling of the COM thread management construct (the apartment) from the synchronization construct (contextual concurrency management) feels conceptually pure and gives us more flexibility: we now can build an object that any thread can enter (as the RTA would have allowed), but we can choose whether to synchronize method invocations (which the RTA would not have allowed).
Figure 4-1 shows all possible synchronization settings for an object. The values are identical to those available for configuring transaction support. However, instead of being enlisted in or beginning a new transaction, an object with the value Supported, Required, or Requires New participates in or begins what is known in COM+ as a synchronization domain. Under MTS, synchronization domains were known as activities and were configurable only through the method a client chose to instantiate another object. Unfortunately, it therefore was up to the client to determine whether a new object could join its activity. Under COM+, this has become transparent to the instantiator and is controlled solely by the setting in the property sheet shown in Figure 4-1.
Figure 4-1. Concurrency tab of the property sheet of a configured thread-neutral object.
Like a transaction, a synchronization domain can include objects in different contexts, apartments, processes, and hosts.4 Also, a synchronization domain is formed through the creation of an object with the setting Required (made by a caller currently outside any synchronization domain) or Requires New (made by any caller); the synchronization domain then flows to any object with the setting Supported or Required at instantiation time. In a synchronization domain, only one physical thread can execute at a time, and each thread must execute as the result of either a direct or indirect synchronous method invocation from the thread that first entered the domain. Figure 4-2 shows a synchronization domain that spans contexts and hosts with several physical threads.
Figure 4-2. Thread and synchronization domain interaction.
Threading Model and Synchronization Interaction
I have championed the fact that synchronization support and thread affinity are independent concepts and now are treated as such by COM+. And it is rather easy to see how synchronization can be applied (or not applied) to objects in either multithreaded or thread-neutral apartments. But understanding how synchronization is applied with the single-threaded apartment is a bit more challenging.5 After all, being single threaded already implies a certain natural synchronization across the entire apartment.The fact is that objects in the single-threaded apartment, which do participate in a synchronization domain, act quite differently from objects that do not participate in a synchronization domain. These differences include the following:
- An object in a synchronization domain will flow domain membership to any object it creates that supports or requires synchronization. As a result, a group of MTA or TNA objects that support synchronization will not experience concurrency when created by an STA object in a synchronization domain. But if the STA object did not participate in a synchronization domain, this group of objects will experience concurrency.
- Synchronized STA objects cannot be entered by calls coming from a causality other than the one of the call chain currently executing within the synchronization domain. Unsynchronized STA objects can.
- STA objects that do not require synchronization will be created in the same apartment as the single-threaded instantiator object. But if the instantiator is not inside a synchronization domain and the STA object requires synchronization, the object actually will be created in a different single-threaded apartment. The reverse does not hold, however. If the caller is inside a synchronization domain, the object will be created in the same apartment—even if it does not support synchronization.
As you can see, for the most part COM+ synchronization layers its benefits on top of single-threaded synchronization. Understanding this relationship certainly will be useful in your own projects.
An object with the synchronization setting Disabled is unaware of synchronization services and behaves like an unconfigured component. Notice that this setting is not the same as Not Supported: the latter ensures that an MTA or TNA object can receive calls concurrently, while the former can result in the object being located in the caller's synchronized context. Disabled also is not the same as the Supported setting, which will force the object into a context that participates in the same synchronization domain as it would were the caller a member of a synchronization domain. But if the object requires a different context than that of the caller (for example, because the object has a different threading model, or because of other COM+ service configurations), the contexts must communicate to prevent concurrent execution. COM+ might achieve this communication by having the contexts share some type of lock. But when the setting is Disabled, the target context will not participate in such a locking
scheme. This is why the Disabled setting truly has a unique meaning.
The Required and Requires New settings mean that the object must run in a synchronization domain. Requires New ensures the object always will be the root of a new synchronization domain. Required creates a new domain only if the instantiator does not already participate in one.
Synchronization Implementation by Deadlock
The theory behind context-based synchronization is fantastic, and the sheer number of options now available to developers should tremendously simplify situations that previously required you to build your own plumbing to achieve just the right concurrency behavior. However, when I examine the current COM+ implementation of the synchronization services for single-threaded objects, my enthusiasm for the technology wanes.
A call into an executing synchronization domain from a caller not participating in that synchronization domain can cause deadlock. A deadlock occurs if the call is made through an object in the synchronization domain whose threading model is Apartment and if that object's thread is currently servicing a message loop (for example, because it is waiting for an Object RPC call return). The message loop does not need to be within the bounds of the object being accessed concurrently for the deadlock to occur. These are the deadlocked entities:
- The caller making the call from outside the synchronization domain.
- The entire apartment of the thread receiving the inbound call of a new causality via its message loop, as well as any upstream callers waiting for the return of method invocations that this thread might be executing on behalf of. Such callers will be deadlocked whether they were part of the synchronization domain or whether the caller representing the initial causality was taking ownership of that synchronization domain.
- The entire synchronization domain of the apartment-threaded object, as well as any threads waiting to enter the synchronization domain.
All these callers now are stalled because the message loop thread attempts to gain access to a lock on behalf of the new inbound caller. This lock never will become available because releasing it would require returning this very thread from a method invocation that is now further up the stack, as shown in Figure�4-3. Hence, the thread never can return from the DispatchMessage call in the message loop, and the call chain becomes stalled from that level upward.
Figure 4-3. Example of a deadlock.
This issue is mitigated by the fact that concurrency often is regulated by a layer of technology in front of the COM+ object layers in modern COM+ architectures. The sharing of object references is discouraged in such highly scalable environments. (For more on this topic, see Chapter 13.) Nevertheless, the issue begs the question of why synchronization support is even an option for STA objects when enforcing it is guaranteed to result in deadlock. It is important to understand the situation precisely, since by its very nature it likely will cause sporadic bugs under just the right timing conditions, and often will be extremely hard to debug. Until Microsoft solves this problem, the only safe thing to do is religiously avoid sharing object references to synchronized STA objects. If you must share object references, be absolutely certain that synchronous calls cannot occur in your architecture. One warning: making concessions at that level of your architecture is likely to introduce brittleness.
The Message Filter
Restricting access for a single-threaded object to one causality at a time is common. Such objects often contain an internal state associated with the operation in progress, and receiving a call unrelated to this current operation can cause failure in these objects. As we have seen, using COM+ synchronization services unfortunately is not yet a solution for this kind of problem. But an ancient mechanism is designed to deal with this type of concurrency: the message filter, which stems from the 16-bit world of OLE and is associated with concurrency in user interface applications. Such applications often share single-threaded objects representing graphical entities among clients. A message filter is intended to prevent access to such single-threaded objects when they perform some internal manipulation (perhaps as the result of an inbound COM+ call), and would become confused by new requests if their internal call sequence had not yet completed.
There is both a server and a client aspect of the message filter mechanism. Server and client applications can install their own message filter by calling CoRegisterMessageFilter, passing an object that implements the IMessageFilter interface:
IMessageFilter�:�public�IUnknown
{
public:
����virtual�DWORD�STDMETHODCALLTYPE�HandleInComingCall(�
��������/*�[in]�*/�DWORD�dwCallType,
��������/*�[in]�*/�HTASK�htaskCaller,
��������/*�[in]�*/�DWORD�dwTickCount,
��������/*�[in]�*/�LPINTERFACEINFO�lpInterfaceInfo)�=�0;
����virtual�DWORD�STDMETHODCALLTYPE�RetryRejectedCall(�
��������/*�[in]�*/�HTASK�htaskCallee,
��������/*�[in]�*/�DWORD�dwTickCount,
��������/*�[in]�*/�DWORD�dwRejectType)�=�0;
����virtual�DWORD�STDMETHODCALLTYPE�MessagePending(�
��������/*�[in]�*/�HTASK�htaskCallee,
��������/*�[in]�*/�DWORD�dwTickCount,
��������/*�[in]�*/�DWORD�dwPendingType)�=�0;
};
On the server side, COM+ will call HandleInComingCall before dispatching an inbound call to an object. On the client side, COM+ will call RetryRejectedCall when the server does not dispatch the call but flat out rejects it or advises retrying it later. The code calls MessagePending when Windows messages are received by the client while waiting for a COM+ call to return. The dwCallType parameter of HandleInComingCall lets the server know whether an incoming call is from a new causality while the object's thread waits for an outgoing call to return (CALLTYPE_TOPLEVEL_CALLPENDING). Such calls therefore are deferred easily (return SERVERCALL_RETRYLATER).
This all sounds marvelous, but there are some limitations. First, the message filter shows its user interface orientation by allowing only local servers, not DLL components, to register a message filter. This means no configured object can use this technique and therefore excludes today's most popular and flexible kind of COM+ component. Second, unless all clients can be guaranteed to be in process, rejecting a call with the retry flag can put an unacceptable burden on the caller. Unless the client's message filter specifies that such a rejected call should indeed be retried, an error message immediately will be returned to the caller. This caller might not be able to set its own message filter—for example, it might have been loaded from a DLL in another process or host, or it might have been implemented in a development system that does not permit setting a message filter. It is not reasonable to expect that all clients either have a message filter that retries or performs the retry at the
level of every single COM+ method invocation. A Visual Basic Standard EXE will install a message filter that does retry for some time and then uses OleUIBusy to bring up the infamous Server Busy dialog box, giving the user a chance to manipulate the user interface of the server application and thus resolve the impasse. But the default COM+ message filter does not do this, instead it propagates all retry rejections directly to the caller. Therefore, even COM+ objects implemented in Visual Basic are subject to this error when operating in a host process implemented with, say, Visual C++.
The bottom line is that message filters were designed in another era, for a problem more specific than regulating general STA object concurrency. If message filters happen to be applicable to your situation, great! By all means use them—they won't go away any time soon. But chances are that forcing message filters into a modern architecture will be like trying to fit square pegs into round holes.
Interception Services
Synchronization is only one service that COM+ performs at the level of the interceptor. COM+ configures lightweight proxies between contexts to perform the specific adjustments to the environment necessary for the crossover. For example, suppose context A has the same synchronization settings as context B but does not support transactions, while context B requires transactions. A lightweight proxy in context A representing an object in context B would create a new transaction but would not attempt to acquire the shared synchronization domain lock. This is part of the reason an object reference can be used only in the context in which it was created.
Now let's take a look at some of the other COM+ services performed by interceptors:
- Figure 4-4 shows the transaction support configuration of COM+. All settings except Disabled are familiar from MTS. Disabled simulates the behavior of an unconfigured object, with respect to transactions. In addition, you now can set the transaction timeout on a per-object basis (rather than a per-machine basis). The following grid shows which combinations of instantiator context and transaction support settings will force a new object into a new context. Note that even if the transaction aspect does not force a new context, one still might be required as the result of other settings.
Caller Context Disabled Not Supported Supported Required Requires New Has transaction x x x x Does not have transaction x x x - Object security configuration is also familiar from MTS. If security is enabled for the application, only users who are members of the roles checked in Figure 4-5 or those roles granted access at the interface and method levels will be able to call the object. An object with security checks enabled always will be created in its own context.
Figure 4-4. Transactions tab of the property sheet of a configured object.
Figure 4-5. Security tab of the property sheet of a configured object.
- Just-in-time activation was present but unconfigurable under MTS. It is now controlled by the similarly named check box shown in Figure�46. When this option is set, the object will be created in its own context because you need a unique interceptor to create the object instance when its first method is invoked.
Figure 4-6. Activation tab of the property sheet of a configured thread-neutral object.
- Selecting the pooling check box shown in Figure 4-6 enables object pooling. Object pooling can be described as the opposite of just-in-time activation: object instances are returned to a pool instead of being destroyed when an object is deactivated. Interfaces related to pooling were defined under MTS (an object could request to be pooled by implementing IObjectControl in a particular fashion), but the pooling mechanism itself had not yet been implemented. Be sure to review the documentation carefully before checking this box: the system makes very specific demands of the implementation of a pooled object. You also can violate the isolation property of transactions by carrying state in a pooled object. Pooling likely will be effective only in somewhat specialized circumstances.
An object's threading model can have an effect on what services will be available to it. For example, single-threaded objects cannot be pooled. Interdependency also exists among certain services. For instance, supporting or requiring transactions (including new ones) forces an object to use just-in-time activation. The specific settings of Supported and Required also force a setting of Required for synchronization support. The transaction setting Requires New implies the setting Required or Requires New for synchronization support. Apart from that, enabling just-in-time activation also demands that the object require an existing or new synchronization domain, regardless of the transaction setting.