Concurrency

The default Enterprise Objects core configuration uses a single thread to perform its operations (not including threads in the Java virtual machine), which means that only a single operation within Enterprise Objects can occur at any given moment. More importantly, however, a single application instance by default has only one access layer stack. This means that each user in a single application instance shares the resources for connecting to, fetching from, and committing data to the application’s data sources with the application’s other users.

If user A initiates a fetch while user B’s fetch is being performed, user A’s fetch must wait until user B’s fetch is done. If user B commits many records to a data source while user A is fetching data from that data source, user B’s commit doesn’t occur until user A’s fetch is done. In an application that services multiple concurrent users, you can see that you’ll likely consider instrumenting multithreading in your Enterprise Objects applications.

This chapter discusses threading in Enterprise Objects, including how to instrument multithreading in an Enterprise Objects Application. It is divided into the following sections:

Determining Requirements

The first step in instrumenting multihreading in an Enterprise Objects application is determining if you really need to. Multithreaded applications of any type are inherently more difficult to program, debug, and maintain than those that use a single thread.

In the context of Enterprise Objects and WebObjects, it is usually better to allocate more application instances and more hardware on which to run those instances rather than to complicate your applications by instrumenting concurrency programmatically. Hardware today is rather cheap and sometimes using more servers is the most reasonable solution to service multiple concurrent users of the same database.

The second step in instrumenting multithreading in an Enterprise Objects application is determining what kind of concurrency you need. This requires knowing (or predicting) the bottlenecks within your application. Do bottlenecks occur at the database level when multiple users attempt concurrent access to the data source so that adding more database channels alleviates the bottleneck? Do bottlenecks occur elsewhere in the access layer so that providing a separate access layer for each user alleviates the bottleneck?

The answers to these questions help determine the mechanism you need to use to instrument concurrency within an Enterprise Objects application. Two common design patterns for concurrency within Enterprise Objects applications are to:

Both of these options provide concurrency within the access layer, which is usually sufficient to achieve multithreading within Enterprise Objects.

The first design pattern is discussed in Providing Separate Stacks. The second is discussed in Database Channels.

Maintaining Thread Integrity

When instrumenting concurrency in any application, you must take responsibility for locking certain objects to ensure thread integrity. Even if you don’t instrument concurrency in an application, that application still uses multiple threads—no Java application is truly single-threaded. On most Java virtual machines, the garbage collector and finalize methods each run in separate threads. To ensure that objects you manipulate aren’t affected by one of these threads, you must lock the objects you manipulate directly. As part of instrumenting concurrency in an Enterprise Objects application, but also in Enterprise Objects applications in which you don’t explicitly instrument concurrency, it is your responsibility to lock the Enterprise Objects you use directly.

In versions of Enterprise Objects prior to WebObjects 5.2, you were expected to explicitly lock and unlock EOEditingContext objects. Most other Enterprise Objects locked themselves in any methods that changed state (which includes most methods) or did not support locking.

These objects, however, which primarily include EOObjectStore objects and fault handlers in the access layer, had no way of knowing the context of their usage. The breadth of the Enterprise Objects API allowed them to be used in many different ways at many different times. For example, faults can be fired in many different scenarios. Consequently, these objects needed to lock and unlock frequently. This has undesirable performance characteristics.

Imagine an EOEditingContext fetches a thousand rows from a database. The EODatabaseContext method initializeObject is invoked once per row to create a corresponding EOEnterpriseObject. That method performs locking operations each time it is invoked. Since EODatabaseContext can service only one EOEditingContext at a time, nearly all of those locking operations are redundant.

A better design is to remove the locking operations from initializeObject and to mandate that the locking operations are instead performed on the EODatabaseContext object. Then, EOEditingContext manages the lock on the EODatabaseContext it uses to fetch data. Only a single set of locking operations on an EODatabaseContext object is required per fetch.

In this design, object stores automatically lock their parent object stores when they perform operations requiring access to those object stores. So, an editing context locks its parent object store once per fetch, making it unnecessary for locking operations to occur in each invocation of initializeObject.

This design reflects the updated concurrency architecture of Enterprise Objects in WebObjects 5.2. In the updated architecture, each lock is treated as a shared resource. To ensure safe concurrent access to Enterprise Objects, it is your fundamental responsibility to lock the Enterprise Objects you use directly. These Enterprise Objects then lock any additional resources they use directly as needed.

Not all classes in the core Enterprise Object layers are suitable for concurrent access within this locking paradigm—this includes Enterprise Object classes that do not implement the NSLocking interface.

For example, EOEnterpriseObject does not implement the NSLocking interface. The framework assumes that enterprise object instances are used only by the thread that has locked their EOEditingContext. Since it probably never makes sense to provide concurrent access to EOEnterpriseObject instances separately from their EOEditingContext, it shouldn’t be a problem that EOEnterpriseObject does not implement NSLocking.

You usually interact only with the EOEditingContext lock. It is vital to properly lock and unlock EOEditingContext objects to ensure the integrity of their EOEnterpriseObject instances. An editing context (an object store) automatically locks its parent object store (usually an instance of EOObjectStoreCoordinator). Obtaining a lock on an EOObjectStoreCoordinator causes it to lock all of its registered cooperating object stores.

Since EOObjectStoreCoordinator is the highest-level object in the access layer stack, and since it automatically locks the object stores registered with it, obtaining a lock on an EOObjectStoreCoordinator is sufficient to manipulate any access-layer objects underneath it. In other words, objects in the access layer can be used only by the thread that has obtained a lock on the object store coordinator that is the highest-level object in that particular access layer stack—you must secure a lock on a particular object store coordinator before using any of the objects it manages.

When you secure a lock for an object store coordinator, it automatically locks all of its registered cooperating object stores. When you release the lock on a particular object store coordinator, it automatically releases the locks it has on its cooperating object stores. If you directly manipulate any access layer objects that are not cooperating object stores that have been locked by an object store coordinator, you must lock and unlock those objects yourself. This includes objects like EODatabaseChannel and EOAdaptorChannel.

What are some of the consequences of irresponsible locking? Consider the case of an invocation of the invalidateAllObjects method on an editing context. This method propagates to every EOEditingContext in an application. If an unlocked EOEditingContext receives an invalidateAllObjects method, that editing context’s EOEnterpriseObjects are forcefully turned into empty faults. The most favorable outcome of this scenario is that any outstanding changes in the application are lost. You can see that it’s important to lock editing contexts (or other Enterprise Objects) to ensure that operations intended for a particular editing context (or other type of Enterprise Object) don’t adversely affect other editing contexts.

Part of the responsibility in managing locks on Enterprise Objects is to responsibly discard locks. You must unlock any locks you explicitly obtain regardless of the circumstances. You can use finally blocks to achieve this requirement. Leaving locks in place after a nonfatal exception eventually deadlocks the application. Locked editing contexts can be garbage collected, so removing references to editing contexts also releases their locks.

Here are a few additional guidelines regarding locking in Enterprise Objects applications:

Problems with locking can be addressed by using NSLog. Set the debug level to at least DebugLevelInformational and the debug groups to include DebugGroupMultithreading. In the event of apparent deadlock, you can obtain a complete stack trace of all the threads within the Java virtual machine by sending the java process the QUIT signal. You can do this on the command line with kill -3pid or Control \, although these commands vary by Java platform.