Academia.eduAcademia.edu

Versioned boxes as the basis for memory transactions

2006, Science of Computer Programming

Science of Computer Programming 63 (2006) 172–185 www.elsevier.com/locate/scico Versioned boxes as the basis for memory transactions João Cachopo ∗ , António Rito-Silva INESC-ID/Technical University of Lisbon, Rua Alves Redol n. 9, 1000–029 Lisboa, Portugal Received 31 December 2005; received in revised form 1 May 2006; accepted 18 May 2006 Available online 4 August 2006 Abstract In this paper, we propose the use of Versioned Boxes, which keep a history of values, as the basis for language-level memory transactions. Unlike previous work on software transactional memory, in our proposal read-only transactions never conflict with any other concurrent transaction. This may improve significantly the concurrency on applications which have longer transactions and a high read/write ratio. Furthermore, we discuss how we can reduce transaction conflicts by delaying computations and re-executing only parts of a transaction in case of a conflict. We propose two language-level abstractions to support these strategies: the per-transaction boxes and the restartable transactions. Finally, we lay out the basis for a more generic model, which better supports fine-grained restartable transactions. The goal of this new model is to generalize the previous two abstractions to reduce conflicts. c 2006 Elsevier B.V. All rights reserved.  Keywords: Software transactional memory; Transactions; Conflict reduction; Multi-version concurrency control 1. Introduction With the increased availability of multiprocessor computers, concurrent programming entered the realms of mainstream programming. Yet, most programming languages lack adequate abstractions for concurrent programming. Mechanisms such as Java’s synchronized keyword are useful to develop thread-safe single objects, but are of little help when various objects are involved. In these cases, to ensure consistency, we want that all operations execute atomically. However, ensuring atomicity for complex domains with lock-based mechanisms is difficult and highly error-prone. Much of the recent work on software transactional memories (STM) [17,9,7,8] deals with this problem by introducing into the programming language abstractions such as transactions, which correspond to the atomic execution of a group of operations. With transactional memories, several transactions can run in parallel using no locks. Instead, either during the transaction or when the transaction commits, a check is made to ensure that the transaction has seen a consistent view of the shared memory. If that is not the case, then the transaction is restarted. Obviously, STMs are most effective when the number of restarts is low compared to the overall number of transactions. Therefore, the number of transaction conflicts should be minimized. In this paper we concentrate on this problem and make the following contributions: ∗ Corresponding author. E-mail addresses: [email protected] (J. Cachopo), [email protected] (A. Rito-Silva). c 2006 Elsevier B.V. All rights reserved. 0167-6423/$ - see front matter  doi:10.1016/j.scico.2006.05.009 J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 173 • We propose the use of software transactional memories based on versioned boxes, which, unlike current approaches, avoids the restart of read-only transactions. A problem with current nonblocking approaches to STMs is that longer-running transactions have a higher probability of being restarted, leading eventually to the transaction’s starvation. Using our approach, long-running transactions that only read never conflict with any other transaction. • We propose the use of speculative read-only transactions as a means to minimize the overheads of software transactional memories in the common case of applications where read-only transactions largely outnumber write transactions. In our approach, it is beneficial to distinguish between read-only transactions and write transactions, because the former may have much less implementation overheads than the latter. Therefore, when there is a high probability of a transaction being read-only, we may optimize its execution by assuming that it is read-only and restarting it later if we find out that the assumption was wrong. • To further reduce conflicts, we propose two strategies: (1) delaying computations, to avoid reading high-contention boxes, and (2) re-executing only the parts of a transaction that caused the conflicts. Using these two strategies it is possible to develop concurrency-friendly objects, which are designed specifically to remove high-contention points from a concurrent program. • To simplify the implementation of the two strategies to reduce conflicts, we extend our STM model with two new abstractions: (1) the per-transaction boxes, which hold values that are private to each transaction, and (2) the restartable transaction, which allows methods to be re-executed at commit time if a conflict occurs because of them. • Finally, we lay out the basis for a more generic STM model based on versioned boxes. The goal of this model is to support restartable transactions that generalize the two abstractions proposed to reduce conflicts. In the following section we describe our proposal for STM based on versioned boxes and discuss its implementation as a Java library. Then, in Section 3 we discuss some scenarios of conflicting transactions and propose the two abstractions that help in the development of concurrency-friendly objects. Those two abstractions are compared and generalized in Section 4. In Section 5 we discuss some related work and in Section 6 we present the conclusions of the paper. 2. Memory transactions based on versioned boxes The distinctive element of our approach to STM is the use of versioned boxes to hold the mutable state of a concurrent program. Versioned boxes can be seen as a replacement for memory locations [7] or transactional variables [8]. 2.1. Model specification Unlike conventional locations, which keep only a single value, a versioned box is a container that keeps a tagged sequence of values—the history of the versioned box. Each of the history’s values corresponds to a change made to the box by a successfully committed transaction, and is tagged with the number of the corresponding transaction. For instance, if a box B is changed three times, in transactions numbered 5, 23 and 87, the history of B will have three values, each tagged with one of the previous numbers. There are two operations that a thread can execute on a versioned box: (1) the read operation, BoxRead(B), which returns the current value of box B, and (2) the write operation, BoxWrite(B, v), which sets the current value of box B to v. The behavior of these operations and how they interact with transactions will be explained below. For now, it is important to note that each of these operations must execute in the context of some transaction. If the executing thread has no current transaction, then a new transaction is created immediately before and is committed immediately after the operation. Transactions, as usual, serve to delimit blocks of operations that should execute atomically. A transaction is associated with exactly one thread – the thread that started it – and lasts until that thread either aborts or commits the transaction. A transaction Tc that starts in the context of another transaction Tp is a nested transaction. Moreover, we say that Tp is the parent of Tc and that Tc is a child of Tp. Transactions that have no parent are called top-level transactions. 174 J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 When a transaction starts, it becomes the thread’s current transaction until it finishes or another (child) transaction starts. When a transaction finishes, its parent, if any, becomes the thread’s current transaction. We say that a box B is read/written in the context of a transaction T when T is the current transaction of the thread executing a read/write operation on B. Besides a reference for its parent, each transaction keeps the following information: (1) a transaction number, (2) a set of boxes that were read in the context of the transaction, and (3) a local-values map that maps boxes to values. To simplify the presentation below, given a transaction T, we will use the notation T-number, T-readSet and T-localValues, to refer to each of these values, respectively. The transaction number represents a version number, which is used both to tag the values on histories and to read the appropriate values from versioned boxes. When a top-level transaction starts, its number is set to the number of the last successfully committed top-level write transaction. So, all the top-level transactions that start between two commits of write transactions have the same transaction number. When a nested transaction starts, its number is set to the number of its parent. 2.1.1. Reading and writing boxes A write operation, BoxWrite(B, v), executed in the context of a transaction T does not change B. Instead, it adds a mapping from B to v to the T-localValues map. This map corresponds to the private storage of transaction T and is used to keep track of the changes performed during T. A read operation, BoxRead(B), executed in the context of a transaction T should return the current value of B. Obviously, the expected behavior for this operation is that it returns the last value wrote to B in the context of T. To accomplish this behavior, the read operation searches for an existing mapping for B in T-localValues. If such a mapping exists, the corresponding value is returned. Otherwise, the search continues recursively on T’s parent, until either one mapping is found or no parent exists. If no mapping was found, then the value to return is obtained from the history of B and B is added to T-readSet. The value obtained from the history of B by the read operation executed in T is the value tagged with the highest number which is less than or equal to the number of T. As we shall see below when we describe the commit of a transaction, this was the last value wrote to B when T started. 2.1.2. Committing and aborting transactions As transactions execute concurrently, accessing shared resources, they may conflict with each other. However, conflicts are detected only at commit time. When a conflict occurs, the commit of some transaction fails. The commit of a top-level transaction T may fail only if T is a read-write transaction. If T is either a read-only or a write-only transaction, then the commit will always succeed. If T is read-only, then no boxes were changed in the context of T. Thus, we can safely assume that the transaction executed instantaneously when it started, because all the boxes read by T are read in the state they were when the transaction started. Even if several transactions committed during the execution of T, the changes made by those transactions are not visible to T. Therefore, T executed in a consistent view of the program state and so it can safely commit. Moreover, as nothing was changed, there is nothing to be done in the commit of a read-only transaction. On the other hand, if T is a write transaction, then at least one box was changed during T, and the changes performed by T should become visible after its commit. Thus, to ensure that transactions are serializable, T can commit successfully if and only if none of the boxes in T-readSet changed after T started — in this case, we say that T is valid. Using this definition, a write-only transaction1 is always valid, because it never obtains values from the history of a versioned box. So, its results will always be the same, no matter when it is executed, and we can assume that it executes instantaneously when it commits. The commit of a valid top-level write transaction T first renumbers the transaction, so that T’s number is one greater than the last successfully committed write transaction. Then, each of the values in T-localValues is added to the corresponding history tagged with this new number. Of course, the check for transaction validity and these changes should be executed atomically. The commit of a nested transaction is simpler, as it just propagates the changes made during the nested transaction to the parent’s context. Specifically, when a nested transaction Tc with parent Tp commits, the elements of 1 We say that T is a write-only transaction if for each box B read in T there is a previous write for B in T. J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 175 public class Transaction { public static void start(); public static void abort(); public static void commit(); } public class VBox<E> { public VBox(E initial); public E get(); public void put(E newE); } Fig. 1. The Transaction and VBox classes. Tc-readSet are added to Tp-readSet, and, also, the mappings in Tc-localValues are added to Tp-localValues, overriding any existing mapping in the parent’s map. The commit of a nested transaction always succeeds. Finally, aborting either a top-level transaction or a nested transaction simply ends the transaction and does not have any effect on boxes. All the transaction’s local-values, if any, are lost. Aborting a transaction always succeeds. 2.1.3. Garbage collecting old values With versioned boxes, old values are not lost; they are kept in the history of the box. This is necessary so that running transactions can read the correct values even after later transactions committed new values to a box. However, as old transactions finish, older values become unreachable. So, unless we want to keep the whole history of changes, the unreachable history values can be safely garbage collected. To capture this concept, we say that a transaction is active if and only if it is either the current transaction of some thread or the parent of an active transaction. Moreover, we say that a value v of a history h is reachable if and only if: (1) v is the most recent value in the history h; or (2) there is an active transaction that started before a value newer than v was added to history h. 2.2. Model implementation We implemented our approach to STMs as a Java library [10], although the facilities we need from Java are available in most other object-oriented programming languages. There are two core classes in the library – the Transaction class and the VBox class – as shown in Fig. 1. However, to simplify transaction demarcation, in our current implementation we provide also a method annotation: the @Atomic annotation. Classes that use this annotation are then post-processed to wrap the annotated methods’ bodies with the boilerplate transaction calls. Using this implementation, it is now easy to develop transactional objects. For instance, a simple transactional counter is shown in Fig. 2. Note the use of a VBox to hold the counter state and the @Atomic annotation on the inc() method. Moreover, this transactional object can be used in larger scale transactions. If inc() is called in the context of another transaction, its body starts a nested transaction and, in the end, commits its changes back to the parent transaction. In the following, we highlight some of the design decisions we made in our implementation. 2.2.1. Implementing versioned boxes We implement versioned boxes using a variation of the Handle/Body idiom [2]. Each versioned box is separated into a handle and a series of bodies. The handle instance represents the versioned box’s identity and its single field is a reference to its most recent committed body. Each body represents a particular version of the box’s state. It has fields to represent the box’s value, a version number and a reference to the body that maintains the previous version of the box’s state. The version number in each body is the number of the transaction that committed the body. For instance, Fig. 3 shows the versioned box of a Counter object that was created during transaction number 4, and that was incremented subsequently, in transactions numbered 8 and 13. 176 J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 public class Counter { private VBox<Long> count = new VBox<Long>(0L); public long getCount() { return count.get(); } public @Atomic void inc() { count.put(getCount() + 1); } } Fig. 2. A transactional Counter object. count body: value: 2 version: 13 next: value: 1 version: 8 next: value: 0 version: 4 next: null Fig. 3. The count box of a Counter instance. Although a VBox keeps a reference to its body, none of the VBox’s methods, including the get and put operations, access the body directly. Instead, all accesses occur through a Transaction object, so that the correct body for the current transaction is used. As we shall see, when a new value is written to a box, a new body is created by the transaction object, but it is not put into the box’s history of bodies until the transaction commits. As a final remark regarding the implementation of the versioned boxes, we note that both the VBox reference to its body, as well as all the slots in the body class are volatile. This is essential to ensure that assignments to these fields by one thread are written in the same order that the fields are assigned to in the thread. These fields are assigned only when a transaction commits a new body for an object. The last assigned field should be the body field in class VBox, so that if a concurrent thread reads the new value of this field, it obtains a body instance consistently assigned.2 This way, threads that access the body field do not need to synchronize with any other. 2.2.2. Implementing transactions In Fig. 4 we show part of the Transaction class’s fields. The static field lastCommitted keeps the number of the last committed transaction, which is needed to give an initial number to each new top-level transaction. The value of this static field changes only when a top-level write transaction successfully commits, in which case it is incremented by one. The localValues map corresponds to the transaction’s private storage. The transaction uses this map to record the new bodies for each box written during the transaction. When, during a transaction, a new value is first written to a VBox, a new body is created with the new value and this new body is inserted into the localValues map. If, during the same transaction another value is written to the same box, then the previously created body is changed to reflect the new value. This is safe, because no concurrent thread can access this new body. Naturally, when a box is read, the localValues map is consulted first to see whether a new body exists for the box. This search is performed recursively in the transaction’s parent until a body is found or until no parent transaction exists. Only if no new body is found in any of the localValues map, will the transaction look for the appropriate 2 This restriction is a consequence of the Java Memory Model [11]. J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 177 class Transaction { static int lastCommitted = 0; int number; Transaction parent; Map<VBox,VBoxBody> bodiesRead = ...; Map<VBox,VBoxBody> localValues = ...; } Fig. 4. The member fields of the Transaction class. body in the box’s history. In this case, the transaction records in the bodiesRead map that the box public history was read. This will be needed at commit time to detect conflicts. The search for the correct body in the public history of a box is a linear search in the sequence of bodies of that box. The search stops once a body with a version number less than or equal to the transaction’s number is found. Given that the history is sorted in descending order of the bodies’ version numbers, the search usually returns the first element in the sequence. Only if a concurrent transaction committed a new value for the box after the current transaction started, will the search need to go further in the sequence of bodies. Note that, even though the box may have already a new value, committed by a concurrent transaction, the current transaction may safely proceed if it is a read-only transaction. Only if we know already that the current transaction is a write transaction can we abort it when a read is performed on a concurrently changed box. Finally, when the transaction commits, if the localValues map is empty, the transaction is read-only and the commit succeeds with no further actions. On the other hand, if the localValues map is not empty, the newly created bodies in it should be installed in the corresponding boxes histories, but only if the transaction is valid. The check for validity verifies that all the entries in the bodiesRead map correspond, still, to the most recent values. According to our model, the check for the transaction validity and the installment of the new bodies should be performed atomically with respect to other concurrent commits. In our current implementation, this is accomplished by forcing all the write transactions’ commits to synchronize on a single object, thereby ensuring the sequential execution of all commits. Note, though, that neither the execution of concurrent transactions nor the commit of readonly transactions are affected by this. 2.2.3. Speculative read-only transactions The implementation we presented thus far registers all the boxes read during a transaction, so that the transaction can be validated in the end. This introduces a significant overhead, both in memory, to store all the map’s entries, and in time, to update that map. Moreover, reading a box forces a search in the localValues map, again introducing a significant overhead. Fortunately, in some cases we can reduce these overheads. Consider the case of a top-level read-only transaction T. As we saw, the commit of T will always succeed, and the bodiesRead map is not used for anything. Likewise, the localValues map is always empty, so the read operation on any box B obtains the result value from the history of B. Therefore, if we know beforehand that a transaction T is read-only, we can eliminate both of the overheads mentioned above. The problem is that, in general, it is not possible to know, until the end of the transaction, that it is read-only. However, we can speculatively assume that a top-level transaction is read-only when it starts. If a write operation is attempted during the execution of the transaction, then we have to abort and restart the transaction as a generic read-write transaction. In programs where read-only transactions largely outnumber read-write transactions, the cost of restarting the erroneously assumed read-only transactions may be much less than the cost of unnecessarily using read-write transactions. One way to improve this strategy is to give the programmer the possibility to give hints about the nature of a transaction. Another, is to adaptively change the nature of a transaction in runtime based on prior executions of the same transactional block. This speculative strategy can be applied to nested transactions as well, but requires some special considerations. If the parent transaction is not read-only, the nested transaction still has to register all the boxes read, so that it can add them to the parent at commit time. Therefore, we can use a nested read-only transaction only when the parent 178 J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 is a read-only transaction. Moreover, when a write operation is executed in a nested read-only transaction we should restart the top-level transaction, rather than the nested transaction itself. 2.2.4. Implementing garbage collection To conclude the discussion on the implementation of the versioned boxes model, we describe how we deal with the garbage collection of old values from the boxes’ histories. As we saw in Section 2.1.3, old values are needed just as long as there are active transactions that may need to access them. Otherwise, when a new value is committed to a box, we could discard the box’s previous value. Our strategy to deal with this problem is to make the transaction that commits a new value for a box be responsible for the cleanup of old values. A transaction that commits new values already has, in its localValues map, the collection of boxes that need cleanup. We just have to know at what time can the cleanup be performed. For that, we use a priority queue of active transactions, sorted by the transaction number, and a cleanup thread that removes inactive transactions from this queue. Whenever a new top-level transaction starts, it is put into the queue, sorted by its number. When a transaction finishes, either by committing or aborting, it is marked as finished and the cleanup thread is signaled. The cleanup thread waits until the transaction which is in the head of the queue (the oldest transaction) is finished. When that happens, the cleanup process removes all the transactions that are already finished from the head of the queue, calling the transactions’ cleanup code as it proceeds. The cleanup of a write transaction consists in going through each of the bodies created by the transaction and discarding their previous versions (by setting the bodies’ next field to null). Because the queue is sorted, when a write transaction is removed from the queue no older transaction is running, so the old values are unreachable and may be safely discarded. 3. Transaction conflicts Now that we presented our proposal for STM, we will discuss some scenarios where transactions can conflict with each other. We start with a simple example that illustrates the benefits of our approach compared to non-versioned approaches. Then, we look into some examples which are hard to solve with current approaches (including ours). In these harder-to-solve examples, many conflicts occur because transactions access some high-contention objects. To solve this problem, we propose that these high-contention objects be reimplemented as concurrency-friendly objects, and show how that can be accomplished through the use of two new transactional abstractions. We say that an object is concurrency-friendly when it is designed to be aware of and to cooperate with transactions, so that it reduces the number of conflicts on the transactions that use it. 3.1. Long-running transactions Consider the typical example of a bank with several bank accounts, where we have two transactional methods: (1) a transfer method that transfers some amount between two accounts, and (2) a totalBalance method that calculates the total balance of all bank accounts. Typically, different transfer transactions involve different accounts, so the probability of conflict between two such transactions should be rather low. However, the trivial implementation of the totalBalance method iterates over all the bank accounts to sum their balance. Thus, without a versioned model, any transfer during the execution of the totalBalance method would force this latter method to abort. Alternatively, all transfers (or any other change to bank accounts) should be suspended until the totalBalance method finishes. Neither option is satisfactory. Using versioned boxes as the basis for STM eliminates this problem, because the totalBalance transaction is a read-only transaction and therefore never conflicts. 3.2. Avoiding conflicts by delaying computations Consider now that we want to keep a record of the total number of transfers made by the bank. An obvious solution would be to use a Counter object (as shown in Section 2.2) and increment the counter during each transfer transaction. Unfortunately, now all transfer transactions will conflict with each other, because all of them read and write the Counter state. The Counter object became a high-contention point in our system because it is shared by most transactions. Can we avoid this problem? J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 179 The increment of the shared Counter object on two transfer transactions causes them to conflict because the first transaction to commit changes the publicly available state of the Counter object, which is read by the other transaction in a previous state. We cannot prevent the first transaction from changing the counter, but we can prevent the second transaction from reading it in a previous state. To see why, note that the behavior of the transfer transaction is not influenced by the value of the counter. Indeed, the value of the counter is read in the inc() method, and neither that value nor the new value is returned to the calling method. Likewise, the execution flow of the inc() method does not depend on the value of the counter. That value is relevant only to determine the new value. So, if we delay the calculation of the new counter value, we can avoid the read of the counter value, thereby preventing the conflict of the second transaction with the already committed transaction. But then, when will the delayed calculation be done? To avoid the conflict, we propose to delay the read of the counter value until commit time, after the transaction has been validated. By this time, if the transaction is valid, we know that all the boxes read during the transaction were not changed meanwhile — thus, all the values read are still the most recent versions. Moreover, because commits of write transactions execute in mutual exclusion, we know that during the commit, no box can be changed. So, it is safe to update the transaction number to the most recent one and to access any box during this time, because the most recent version of every box is consistent with the currently committing transaction. The key insight here is that, at this time, the current value of the counter is the value written by the last committed transaction, rather than the value that the counter had when the current transaction started. So, we can calculate the new counter value and commit this new value. In order to do this we just have to know how many times the inc() method was called during the transaction (and the current value of the counter, of course). Summarizing, to reimplement the Counter object in such a way that it is no longer a high-contention point (in the scenario presented above), we need to: (1) remember how many times the inc() method was called during a transaction, and (2) delay the calculation of the new counter value until commit time (if possible). We shall now see how to implement this. 3.3. Delaying computations with per-transaction boxes To implement the strategy of delaying some computations until commit time, we propose a new abstraction in our model — the per-transaction boxes. The key idea behind per-transaction boxes is that they serve to hold values private to each transaction. These private values may then be merged with shared values, available on versioned boxes, when the transaction commits. To accomplish this, each transaction keeps track of the value of each per-transaction box accessed during the transaction. When the top-level transaction commits,3 the transaction calls the commit method on each of the pertransaction boxes, with the current per-transaction box value. Typically, the commit method will merge the pertransaction value received with the values of some versioned boxes. However, the responsibility of writing this method is left to the programmer that uses the per-transaction box. The additional power given by this new abstraction stems from the fact that, when the per-transaction box commit method executes, the transaction has been validated and its number is set to the appropriate final number. Therefore, if the method reads some versioned box B, it will use the last committed value on B, rather than the value that B had when the transaction started. This last value of B, as we saw on the previous section, is necessarily consistent with the transaction. Using this new abstraction, we can implement a new version of the Counter class that delays the increment, as shown in Fig. 5. In this new version, the inc() method no longer reads the value of count. Instead, it updates a pertransaction box which holds the number of times that the inc() method was called. When the transaction commits, the value of the per-transaction box is added to the count value. Obviously, the new getCount() method needs to take the toAdd value into account. The good thing about this new counter is that it causes no conflicts in the transfer transactions anymore. Moreover, all the changes were made in the counter class and the transfer method remains the same. The Counter class is the simplest example where this approach of delaying the computations eliminates conflicts. More interesting cases include the addition and removal of elements to collections, for instance. In this case, we would 3 The commit of a nested transaction simply propagates the per-transaction values to the transaction’s parent. 180 J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 public class Counter { private VBox<Long> count = new VBox<Long>(0L); private PerTxBox<Long> toAdd = new PerTxBox<Long>(0L) { public void commit(Long value) { count.put(count.get() + value); } }; public long getCount() { return count.get() + toAdd.get(); } public @Atomic void inc() { toAdd.put(toAdd.get() + 1); } } Fig. 5. A Counter object that delays its increment. use two per-transaction boxes: one to hold the elements to add, and another to hold the elements to remove. Both of these boxes would be changed by the addition and removal operations, and the commit of the boxes would have to perform the delayed additions and removals. Like in the Counter case, a collection using this approach can prevent the conflict between two concurrent transactions that change the same collection, because the access to the underlying structure of the collection is delayed until commit time. Naturally, the conflict might still exist if the concurrent transactions need to read the collection also. Likewise, if a transaction using a Counter object needs to read the value of the counter, then the delay of the inc() operation is of no use to prevent the conflict with a concurrent transaction that behaves the same. To read the counter’s value, the getCount() method has to read the count box. We shall address these cases in Section 4. To conclude the presentation of the per-transaction boxes, we note that the drawback of this approach is that the execution of the per-transaction boxes’ commit methods increases the time of the commit operation, which executes in mutual exclusion with all the other commits of write transactions. However, neither the execution of other transactions nor the commit of read-only transactions is influenced in any way by the execution of the per-transaction boxes’ commit methods. 3.4. Removing conflicts by restarting computations The strategy used to reimplement the Counter class works whenever we can eliminate read operations. That happens when the values returned by those read operations are not needed to decide on the execution flow of the transaction. We shall now discuss some cases where this is not possible. Consider an implementation of a singly-linked list that uses versioned boxes to keep the list forward links, and which contains the following methods: (1) the remove(Object) method that removes an object from the list, and (2) the contains(Object) method that returns a boolean value indicating whether the given object exists in the list. Furthermore, suppose that we have a list L, with two objects O1 and O2, that is used by two concurrent read-write transactions T1 and T2. If T1 removes O1 from L and commits before T2, which calls contains(O2), then, on a typical implementation of these methods, T2 will have to abort because L’s structure changed. In particular, the head of the list now points to the node with O2, rather than the node with O1. However, after the changes made by T1, the result of contains(O2) is the same, because the list still contains O2. Therefore, if T2 started after T1 committed, it would produce the same results. So, T2 can be serialized after T1 and there should be no conflict. Unfortunately, in this case we cannot delay the computation of contains(O2), because the whole point of using this method inside a transaction is to know whether the list L contains O2 or not — presumably, to decide on what J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 181 class List { ... @Restartable boolean contains(Object obj) { ...contains body... } } Fig. 6. Using restartable transactions. to do next. We expect this to be true for every method that returns some value and does not have side-effects. Thus, per-transaction boxes do not help us here. The rationale presented above for the conclusion that T2 should not conflict with T1, was that the result of the operation that caused the conflict would be the same if that operation executed after T1 committed. So, it seems that we can remove the conflict, if we re-execute the contains(O2) call at commit time, and the re-execution produces the same result. If the result is not the same, then the transaction execution might have been different, and therefore we must restart the transaction. The advantage of this strategy is that it avoids the restart of the whole transaction, by re-executing just a smaller part of the transaction. However, unlike the restart of the whole transaction, the re-execution occurs at commit time, which means that it blocks the remaining commits during its execution. Obviously, there is a tradeoff here that needs to be evaluated carefully. Nevertheless, we expect that for longerrunning transactions, with small blocks of restartable operations, this strategy pays off. More so, if those blocks are high-contention points, because, in that case, they would force the transactions to execute almost sequentially anyway. 3.5. Restartable transactions To deal with the problem identified in the previous section we propose a new abstraction: the restartable transaction. From the programmer’s point of view, this new abstraction can be introduced as a new annotation, @Restartable, to delimit this new kind of transactions. Fig. 6 illustrates its use in the example of the List class. The semantics of this construction is an extension of the @Atomic annotation’s semantics, introduced in Section 2.2. Like the previous one, it serves to demarcate a transaction. However, it also serves to give a hint to the transaction system that if a conflict occurs because of boxes read exclusively within this transaction, then the transaction can be re-executed at commit time to see whether it produces the same value as in the first execution, thereby resolving the conflict. However, not all transactions can be restartable. If the transaction is not restartable, the @Restartable annotation behaves like the @Atomic annotation. Here, we restrict restartable transactions to nested read-only transactions implemented by a method. In this case, the only influence of the restartable transaction on the execution of its parent is the method’s return value. In the following section, we will address the case of restartable transactions with sideeffects. To implement restartable transactions, we need to capture the following information: • The method body with all its arguments, so that we can re-execute it later. • The method return value, so that we can compare it with the result of the re-execution. • The current state of the enclosing transaction, including all of its ancestors. This state consists of a copy of all the local-values maps of the restartable transaction’s ancestors at the time that it started executing for the first time. This is needed, so that the re-execution can access the same values of the boxes written by the enclosing transaction. To capture the method body with all its arguments (including the this), the @Restartable annotation can be expanded as shown in Fig. 7. The RestartableTransaction object should obtain the set of boxes written in the current transaction, create a new nested transaction (having the current transaction as its parent) and execute the run method within the context of this newly created nested transaction. If the run method terminates normally and it did not write to any box, 182 J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 class List { ... boolean contains(final Object obj) { return new RestartableTransaction<Boolean>() { public boolean run() { return restartable$contains(obj); } }.execute(); } boolean restartable$contains(Object obj) { ...contains body... } } Fig. 7. Implementing restartable transactions. then a restartable transaction record is created, with all the relevant data, and this record is added to the current transaction. Otherwise, the nested transaction is committed or aborted as usual. Naturally, the restartable transaction records should be propagated through the transaction hierarchy, as transactions commit. They will be used only at commit time by the top-level transaction. Restartable transactions hide the read operations performed during their execution from their parents. Therefore, when the top-level transaction commits, the validity check will not take into account the boxes read during restartable transactions. However, after the top-level validity check passes, each of the restartable transaction records will be validated in turn. A restartable transaction record is valid if none of the boxes read during the restartable transaction have changed. This is similar to the validity check of a top-level transaction. If a restartable transaction record is not valid, then it is re-executed in a newly created nested transaction. If the re-execution terminates normally, produces the same result as before, and does not write to any box, then the changes that occurred in the boxes do not affect the top-level transaction and it can continue. Otherwise, the commit of the top-level transaction should fail. The key point here is, again, that the re-execution of each restartable transaction occurs during the commit of the top-level transaction, after it has been renumbered. This way, each re-execution will see the most recent values for the shared resources, much in the same way as the commit methods of the per-transaction boxes. Moreover, because the top-level transaction is valid, all the values read during the re-execution of the restartable transaction are consistent with the values read previously by the restartable transaction’s ancestors. Finally, the drawback of this approach to resolve conflicts is the same that we referred to at the end of Section 3.3: The re-execution of restartable transactions at commit time may introduce huge overheads in the execution time of the commit operation. 4. Towards a model for fine-grained restarts The restartable transaction abstraction proposed in Section 3.5 allows fine-grained control over which parts of a transaction can be re-executed, provided that those parts do not change any boxes. In this section, we begin by discussing the implications of expanding this abstraction to include write operations. Then, we explore an extension to the model presented in Section 2 that is better suited to implement fine-grained restartable transactions. 4.1. Delaying computations versus restarting transactions The first strategy we used to eliminate conflicts was to delay some computations until commit time. Then, we introduced the notion of restartable transaction, which, again, works by making some computation at commit time. J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 183 This apparent similarity raises the question of whether there is any relation between the two abstractions. More specifically, we would like to know whether restartable transactions can replace the use of per-transaction boxes. We used the per-transaction boxes to implement a new counter because the counter’s inc operation was causing many conflicts. Similar reasons led to the use of restartable transactions to implement the list’s contains operation. So, it is tempting to use this later abstraction to implement the counter’s inc operation, because it is simpler to use. Unfortunately, if we try this, we face the problem that the inc operation writes to a box. Therefore, to unify the two approaches, we need to extend the restartable transaction’s semantics to accommodate write operations. 4.2. Restartable transactions that write The generic necessary condition for having a restartable transaction is that its re-execution in a different context (at commit time) does not change the execution of the top-level transaction. A conservative approach to ensure this condition is to require that the restartable transaction returns the same result as before and does not perform any side-effects. This last restriction eliminates all the changes to shared resources. However, this approach is overly restrictive. A straightforward change in the restartable transactions’ semantics is to accept transactions that write new values to versioned boxes, provided that those boxes are never read in the remaining of the enclosing top-level transaction. This restriction applies both during the execution and the re-execution of the transaction, because, when a restartable transaction re-executes, different boxes may be written. This change is sufficient to allow us to replace the per-transaction box by a restartable transaction in the simple case presented in Section 3.2. But if we consider a case where the counter is incremented twice in the same transaction, then the implementation based on a restartable transaction fails to eliminate the conflict, although the per-transaction box’s solution still works. The restartable transaction approach fails in this case, because the value of the count box written by the first inc operation is used later (in the second inc operation). Therefore, a different value for that box can change the behavior of the top-level transaction. And it does, indeed, because the second increment would produce a different result. However, the second inc operation is a restartable transaction, also. So, if it re-executes after the re-execution of the first restartable transaction, then the correct value would be produced. This later observation leads to another change in the semantics of restartable transactions: A restartable transaction may write a versioned box if, during the execution of the enclosing top-level transaction, that box is read only by restartable transactions, in which case, the later restartable transactions should be re-executed whenever the value of the box is changed. Although informally specified, this new semantics seems to be expressive enough to replace the use of the per-transaction box in the Counter class. The problem with this extended semantics is that it is much harder to implement in our current model than the relatively simple per-transaction boxes. The difficulty stems from the fact that we need to keep track of which transaction produced a particular value for a box, together with the need to capture the currently written boxes whenever a restartable transaction starts. 4.3. Extending the versioned model In our current model, if several nested transactions (of the same transaction) write to a box B, they do not produce different versions for B. Instead, they replace the previous value of B with the new value. This can be done, because nested transactions execute sequentially, so there is no need for older versions. However, if we take into account restartable transactions, which may need older values when they re-execute, then it makes sense to use the same approach of keeping a different version for each committed transaction, independently of its nesting. Moreover, this approach extends naturally to allow concurrent nested transactions. The fundamental change we propose here is to treat nested transactions in the same way as top-level transactions. The only difference between the transactions becomes the context where they execute. The execution of transactions inside other transactions forms a transaction tree, where each transaction in the tree keeps the information about what was read and what was written by itself, rather than merging this information with its parent. The advantage, for restartable transactions, of this new approach, is that they become much more easy to implement. First, because all the execution contexts needed to re-execute the transaction are kept by the transaction tree. Second, because the re-execution of the transaction needs to concern only with updating its local values, rather than 184 J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 with propagating the changes to its parent. Finally, because it becomes trivial to record the dependencies between different transactions. With this approach it is feasible to consider that all transactions are restartable. Therefore, when a transaction conflicts with another, it is possible to determine the smallest portions of code that need to be restarted to remove the conflict. Naturally, any restart may fail to produce the same results, which would recursively cause a higher-level transaction to restart, until we eventually restart the top-level transaction. One interesting aspect of this approach is that the transaction granularity greatly influences the ability to recover from a conflict. Furthermore, we believe that the use of fine-grained restartable transactions allows a graceful degradation from an optimistic concurrency control policy to a pessimistic policy, but only when needed. 5. Related work Compared to other approaches to STM, e.g. [17,9,7,8], the distinctive feature of our model is the use of multiple versions for each memory location. Therefore, our approach is better suited for long-running read-only transactions than the rest. This comes at the expense of more memory overheads. The use of multiple versions to increase the concurrency of transactional systems is well known in the area of database management systems. Since the seminal work of Bernstein and Goodman [1] on multi-version concurrency control, the technique of using multiple versions as the basis for optimistic concurrency control was applied in several contexts. One such application was made by Graham and Parker [4]. They presented a formalism for describing multiversion object base systems which is similar, at the model level, to our proposal. However, their work is in the context of object databases, which have different concerns compared to a programming language level software transactional memory. As far as we know, our STM proposal is the first to incorporate the ideas of multi-version concurrency control in this context. The idea of keeping a history of values for each object whenever it is changed, rather than replacing the old value, was used by Reed in the context of the distributed execution of atomic actions [13,14]. However, many of his concerns regarding the difficulty of synchronization on a distributed system do not apply in the context of STM. Moreover, in Reed’s approach, when an object is read, the history of the object may be updated by that read operation, thereby introducing a point of synchronization among concurrent read-only transactions, which defeats somehow the benefits of this approach. The semantics of nesting in our model corresponds to what Moss and Hosking [12] describe as linear nesting. This simplified model allows a simpler implementation of nesting and is sufficient to support the retry and the orElse operations from [8]. Although we did not address these operations in this paper, their implementation in our model is quite straightforward. A notable difference of our implementation is that it uses a single lock to ensure mutual exclusion during the commit, whereas others, e.g. [9], rely on a single CAS operation to perform the commit, because all the values are put in place during the transaction’s execution. We may use a similar strategy in the future. Regarding the reduction of transaction conflicts, Herlihy et al. [9] proposed the use of contention managers, which decide whether conflicting transactions should abort other transactions or wait for them to finish. Since then, several proposals of different contention management policies have been proposed [6,15,16]. By contrast with this work, our approach is to eliminate conflicts by providing abstractions that allow the implementation of concurrency-friendly objects. We believe that this approach is better suited to the development needs of a common programmer, as she can concentrate on specific objects, involved in specific transactions, instead of general contention management policies. Nevertheless, we believe that the two approaches complement each other, but more research is needed to determine how they should be combined. Moreover, although we presented the discussion on the reduction of conflicts integrated with our model of versioned boxes, most of what we propose applies equally well to other STM approaches. For instance, the per-transaction boxes do not depend on the existence of versions and, therefore, could be used with any other STM, provided that there are some means to execute arbitrary code during the commit of a transaction. Finally, the idea of re-executing part of a transaction during the commit is similar to the use of the field calls technique to reduce the lock duration in traditional database transaction processing [5]. J. Cachopo, A. Rito-Silva / Science of Computer Programming 63 (2006) 172–185 185 6. Conclusions In this paper we propose a new approach to implement software transactional memories, based on boxes that hold multiple versions of their contents. This allows the execution of read-only transactions that never conflict with other concurrent transactions. We implemented this approach in Java, as a library, and in the paper we discuss some of the design decisions of this implementation. The source code of our implementation can be found in [10]. The JVSTM is being used in a production environment by the FénixEDU project [3]. It replaced all the concurrency control mechanisms based on locks in this web application, with significant improvements on the perceived application performance. Versioned boxes were used to store each field of each one of the 240 domain classes, including all the collections used to store the 320 bidirectional associations between domain classes. The database has approximately 1 GB of data, and the application is deployed in a cluster with three servers with two processors each. To improve the concurrency of high-contention transactions, we propose in this paper two new abstractions: the per-transaction box and the restartable transaction. These new abstractions facilitate the development of transactional objects that allow the reduction of conflicts. The per-transaction box mechanism is implemented in the JVSTM and is being heavily used in the FénixEDU project to reduce the conflicts on the collections used to implement the domain relationships. Finally, we discuss an extension to the STM model that aims at providing better support for implementing restartable transactions. Acknowledgements We would like to thank the anonymous reviewers for their insightful comments that helped us to improve this paper. Also, we would like to thank the Fénix team, for their support in putting our STM model to work in a production environment. Finally, we thank INESC-ID’s Software Engineering Group, and especially to António Leitão, for the fruitful discussions during our group meetings. References [1] P.A. Bernstein, N. Goodman, Multiversion concurrency control — theory and algorithms, ACM Transactions on Database Systems 8 (4) (1983) 465–483. [2] J.O. Coplien, Advanced C++ Programming Styles and Idioms, Addison-Wesley, Reading, MA, USA, 1992. [3] FenixEDU, FénixEDU, 2005, Home page at: http://fenixedu.sourceforge.net. [4] P. Graham, K. Barker, Effective optimistic concurrency control in multiversion object bases, in: E. Bertino, S. Urban (Eds.), Proceedings of the International Symposium on Object-Oriented Methodologies and Systems, vol. 858, Springer-Verlag, 1994, pp. 313–328. [5] J. Gray, A. Reuter, Transaction Processing: Concepts and Techniques, Morgan Kaufmann, Los Altos, CA, USA, 1992. [6] R. Guerraoui, M. Herlihy, S. Pochon, Toward a theory of transactional contention management, in: Proceedings of the 24th Annual ACM Symposium on Principles of Distributed Computing, ACM Press, Las Vegas, NV, USA, 2005. [7] T. Harris, K. Fraser, Language support for lightweight transactions, in: Proceedings of the OOPSLA 2003 Conference on Object-Oriented Programming, Systems, Languages, and Applications, in: SIGPLAN Notices, vol. 36, ACM Press, Anaheim, CA, USA, 2003, pp. 388–402. [8] T. Harris, S. Marlowe, S. Peyton-Jones, M. Herlihy, Composable memory transactions, in: Proceedings of the ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, ACM Press, Chicago, IL, USA, 2005. [9] M. Herlihy, V. Luchangco, M. Moir, W.N. Scherer III, Software transactional memory for dynamic-sized data structures, in: Proceedings of the 22nd Annual ACM Symposium on Principles of Distributed Computing, ACM Press, New York, USA, 2003, pp. 92–101. [10] JVSTM, JVSTM, 2005, Home page at: http://www.esw.inesc-id.pt/˜jcachopo/jvstm. [11] T. Lindholm, F. Yellin, The Java Virtual Machine Specification, 2nd edition, Addison-Wesley, Reading, MA, USA, 1999. [12] J.E.B. Moss, A.L. Hosking, Nested transactional memory: Model and preliminary architecture sketches, in: Workshop on Synchronization and Concurrency in Object-Oriented Languages, SCOOL05, San Diego, USA. Available at: http://hdl.handle.net/1802/2099, October 2005. [13] D.P. Reed, Naming and synchronization in a decentralized computer system, Ph.D. Thesis, MIT, Cambridge, MA, USA, 1978. [14] D.P. Reed, Implementing atomic actions on decentralized data, ACM Transactions on Computer Systems 1 (1) (1983) 3–23. [15] W.N. Scherer III, M.L. Scott, Contention management in dynamic software transactional memory, in: Proceedings of the ACM PODC Workshop on Concurrency and Synchronization in Java Programs, St. John’s, NL, Canada, July 2004. [16] W.N. Scherer III, M.L. Scott, Advanced contention management for dynamic software transactional memory, in: Proceedings of the 24th Annual ACM Symposium on Principles of Distributed Computing, ACM Press, Las Vegas, NV, USA, 2005. [17] N. Shavit, D. Touitou, Software transactional memory, in: Proceedings of the 14th Annual ACM Symposium on Principles of Distributed Computing, ACM Press, Ottawa, Canada, 1995, pp. 204–213.