• No results found

ming languages. Then, comparable to the database world, the same situation arises when a crash happens during the recovery of a persistent application.

4.3 Transaction Locking Levels

According to Coulouris in [35] there are three ways to provide concurrent access control to shared data. The rst option is to use locking to grant or deny ac- cess to shared data, based on the already granted locks on the object. This is the option currently used in the transactional implementation. Details of this implementation follow in Section 4.3.1 to Section 4.3.6. The second option is to use timestamp ordering and hence avoid deadlocks. This option is stated as an alternative in Section 4.3.7. The third option is to use optimistic concurrency control. In this last option all operations are performed on copies of the shared data, called tentative data. Just before committing the operations, a check is performed to validate if the original values were not simultaneously updated by another concurrent process. A validation of the option to use tentative objects in terms of object-oriented programming languages is still part of future work.

4.3.1 The Need for Locking Levels on Transactions

Until now, we only focused on the atomicity property of database transac- tions. But in Section 3.2.1, isolation is mentioned as one of the key properties of database transactions. We will use this isolation property to dene concepts of advanced transaction types. In Chapter 3 we used transactional methods in sin- gle user, single threaded object-oriented programs, this way avoiding data races. Isolation of data was always ensured. There were no other processes which could access the current data while we were in the middle of our transactional method. In this section we translate the isolation property into locking levels dened on transactional methods. We will use these locking levels to relax the isolation properties, allowing multiple threads to simultaneously access the same object.

We introduce these locking levels as language concepts. In the case study we validate if these locking levels can be used as a language concept to qualify the locking of transactional methods.

4.3.2 Dierent Locking Levels on Transactional Methods

We dene three dierent locking levels on transactional methods: readonly, shared and exclusive.

readonly: These are the eects when the transactional method locks the implicit object in readonly mode.

• The transactional method itself has readonly access to the implicit object.

It is not changing any values of the implicit object, only reading them.

• Other transactional methods can read or write the values of this implicit

object.

shared: These are the eects when the transactional method locks the implicit object in shared mode.

• The transactional method itself has full access to the implicit object. It is

not concerned with others reading the values before the current transac- tional method nishes.

• Other transactional methods can read the values of this implicit object, but

they cannot change these values.

exclusive: These are the eects when the transactional method locks the im- plicit object in exclusive mode.

• The transactional method itself has full access to the implicit object. It

is probably changing the values of the implicit object, or it does not want other transactional methods reading or writing the values before the current transactional method nishes.

• Other transactional methods cannot read or write the values of this implicit

object.

• This is the default behavior considered in the transactional methods of

Chapter 3.

4.3.3 Transactional Methods Requesting Locking Levels

Before a transactional method can start its execution, it needs to request the lock on the implicit object. A dierent approach is taken depending on the locking level needed and the current locks granted to other transactional methods on the same implicit object. The approaches are shown in Table 4.1. The top row shows the type of already granted locks to other transactional methods. The rst column respectively, shows the requesting lock of the current transactional method. The cells in the table indicate Yes when the lock can be granted to the current transactional method. Depending on the implementation of the locking level, two strategies can be used in case of a No indication. The current implementation uses a deny strategy, causing the current transactional method to abort. Another option is to use a wait strategy, causing the current transactional method to wait for the lock to become available. This strategy can lead to long waiting times and possible deadlocks.

4.3. TRANSACTION LOCKING LEVELS 85

No

Lo

cks

Readonly Shared Exclusiv

e A lre ady Gr ante d Lo ck

Readonly Yes Yes Yes No

Shared Yes Yes No No

Exclusive Yes No No No

Requesting Lock

Table 4.1: Transactional Method Requesting Lock on the Implicit Object The rst column shows that when there are no locks existing on the implicit object, the current transactional method is granted all locking levels.

The second column shows that when a readonly lock is already granted, the current transactional method cannot get an exclusive lock any more. However, a request for a shared or a second readonly lock is granted, since the current readonly lock allows others to read and to change the values of the implicit object.

The third column shows that when a shared lock is already granted, the current transactional method can only get a readonly lock. Or rephrased, before the transactional method holding this shared lock nishes, others are allowed to read the values.

The fourth column shows that when an exclusive lock is already granted, other transactional methods cannot get any lock.

4.3.4 Introducing Concepts for Locking Levels

By default, transactional methods lock the implicit object in exclusive mode. The developer can choose to relax the isolation property of a transactional method by mentioning the relaxed locking mode in the header declaration, as shown in Algorithm 32.

Algorithm 32 Exclusive, Shared and Readonly Transactional Methods

public transactional void changingMethod() { // implicit object locked in exclusive mode }

public shared transactional insignificantChangingMethod(){ // implicit object locked in shared mode

}

public readonly transactional void inspectingMethod(){ // implicit object locked in readonly mode

}

For the rst transactional method changingMethod, no locking mode is men- tioned, default exclusive locking mode is used. The second transactional method insignificantChangingMethod, requests the shared locking mode. The last transactional method inspectingMethod, will not change any values of the im- plicit object and requests the readonly locking mode.

4.3.5 Multi-Threading in Object-Oriented Programming

Languages

Modern object-oriented programming languages like Java support concepts for multi threading. These concepts look similar to the concepts of locking levels introduced here. However, we decided to introduce new concepts instead of using the existing concept synchronized. The Java language manual describes synchronized methods as follows:

First, it is not possible for two invocations of synchronized meth- ods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the rst thread is done with the object.

Second, when a synchronized method exits, it automatically estab- lishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.

This is not exactly the behavior we are expecting from the exclusive locking mode in multi-threaded applications as shown in Chapter 5. The rst rule of the description states that a synchronized method can not interleave on the same object. This looks like the behavior we are expecting, but then it states that other methods can continue their execution when the rst method is done. We will use exclusive locking in Section 5.2, where we will be propagating the locks of prepared-to-commit child transactional methods to their parent transactional

4.3. TRANSACTION LOCKING LEVELS 87 methods. When the synchronized keyword is used, this lock on the implicit object is released when the child transactional method nishes, instead of keeping the lock to pass it to the parent transactional method.

In summary, the granularity of locks provided by the synchronized keyword is too ne-grained to support transactional methods, which typically comprise multiple method calls.

4.3.6 Locking Implementation

The ultimate goal is to oer these locking levels as an extension of the object- oriented programming language Java. Extending the Java compiler to support these concepts potentially leads to extending the virtual machine. As a prove of concept, the strategy used to implement locking is comparable to the strategy used to implement the transactional keyword in Section 3.6. Instead of chang- ing the virtual machine and adding new byte code constructs for new language features, the language features are translated into existing byte code with the desired semantic result.

The locking level is currently implemented as an annotation @shared or @readonly, then during execution aspects intercept and insert the necessary code. A before aspect injects code, before the transactional method is executed, to request the lock on the implicit object. When this lock is unavailable, an exception is thrown and the transactional method aborts. An after aspect in- jects code, after the transactional method nishes by committing or aborting, to release the previously requested locks. The deny strategy was implemented this way, the wait strategy could as well be implemented this way.

It is the authors vision that stating a certain behavior as expected for a specic concept, should always be covered by a number of tests, this way dynamically val- idating this behavior. Using the combination of annotations and aspect-oriented programming, the concepts introduced in this dissertation are evaluated dynam- ically. Tests suites are created using these concepts, the suites act as a safety net for the complete implementation. This safety net has proved its adequacy several times during the evolution to the current version of the concepts.

4.3.7 Avoid Deadlocks Using Timestamps

In the current implementation the deny strategy is used for the locking mode. Using this strategy, transactional methods abort immediately if the lock is not granted. If the wait strategy is used, parallel threads wait until the lock becomes available. This way, long waiting times and potentially deadlock situations can arise if more than one thread requests an exclusive or shared lock on the same implicit object.

This section elaborates on another strategy to implement the locking mode. By using timestamps to decide when read and/or write conicts occur, data

races between dierent transactions are avoided. This strategy is based on the timestamps used in the database world [10, 11], but altered to be implemented in terms of transactional methods in object-oriented programming languages. The timestamps strategy is described in detail here, validation of this strategy is part of future work.

A timestamp can be implemented as a counter; each time a timestamp is requested the counter is incremented. Each transactional method gets its times- tamp Ti at startup. Each object has two timestamps, namely one read timestamp R and one write timestamp W. The read timestamp R is the largest timestamp of any transactional method which successfully executed a read operation on the object. The write timestamp W is the largest timestamp of any transactional method which successfully executed a write operation on the object.

When a transactional method issues a read operation on an object, it veries if the timestamp of the transaction Ti is less than the write timestamp W of the object (Ti < W ). If so, this means that the object is changed since the transactional method Ti started by another transactional method Tj and the current transactional method Ti then aborts. In the database world this is known as the dirty read problem. If on the other hand, the timestamp of the transactional method Ti is greater than, or equal to, the write timestamp of the object (Ti >= W ), the read operation in Ti is executed. The read timestamp R of the object is then set to the maximum of the Ti and the current value of R or max(Ti, R).

When a transactional method issues a write operation on an object, it rst veries if the timestamp of the transaction Ti is less than the read timestamp R of the object (Ti < R). If so, this means the object was previously needed by another transactional method Tj, this other transactional method Tj counts on the fact that it has read a correct value. The current transactional method Ti should abort. If Ti is greater than or equal to R, the next test is to verify if Ti is less than W. This means that the object is already written by another transactional method Tj, so the current transactional method Ti aborts. Only if Ti is greater than or equal to R and Ti greater than or equal to W, the write operation is executed, and W and R are replaced by the value Ti.

Working with timestamps is used in the database transactional world to reach freedom of deadlocks, no transaction ever has to wait.