Keywords

1 Introduction

Starting from the 1960s, reversible computing has been studied in several contexts ranging from quantum computing  [6], biochemical modelling  [7], programming  [8, 9], and program debugging  [10, 15]. Distributed reversible actions can be seen as defeasible partial agreements: the building blocks for different transactional models and recovery techniques. The work of Danos and Krivine on reversible CCS (RCCS)  [1] provides a good example: they show how notions of reversible and irreversible actions in a process calculus can model a primitive form of transaction, an abstraction that has been found useful, in different guises, in reliable concurrent and distributed programming. Since the seminal work of  [1], other works have investigated the interplay between transactions and reversibility [2, 11] in the area of message passing systems. On the shared memory side, we just recall the work of [12] where a CCS endowed with a mechanism for software transactional memories (STMs) is presented. Another work about reversibility and a high-level abstraction of shared memory (tuple spaces) is presented in [16].

Software Transactional Memory  [3, 4] is an elegant way to address the problem of concurrent programming, by relieving the programmer from the burden of dealing with locks. The lock-based approach is error prone and usually leads to deadlocks when the complexity of the system grows. Opposite to the lock-based approach, STM uses transactions. A transaction is a block of code accessing shared data which is meant to be executed atomically with an “all or nothing” policy: that is either all the effects of a transaction have to be visible when it commits, or none of them has to be visible in case of abortion. This abstraction allows for multiple transactions to be executed “at the same time”. The programmer just needs to specify the sequences of operations to be enclosed in transactions, while the system is in charge of the interleaving between the concurrent transactions. A transaction can either commit and update the system permanently or abort and discard all the changes done by its execution.

In this work, we are interested in the interplay between reversible computing and the STM approach to control the concurrent executions. Therefore, we present a formal framework for describing STMs in a simple shared memory context. In particular, when a transaction aborts, it is necessary to discard all the updates that it made and we need to bring the system back to the state before the execution of the transaction. To accomplish the behaviour above, we implement a rollback operator following the approach given in  [13]. A transaction can access a shared variable either in read or in write mode. Given this, different policies can be used to regulate the transactions which are accessing the same value in the shared memory. According to the implemented policy, some transactions will succeed and some will be aborted. We will show how it is possible to model writer and reader preference  [5] in our framework. Consider the following C-like code where two functions/threads access the same shared variables:

figure a

All the possible executions of the two functions are reported above: either the two functions are executed sequentially or are interleaved (leading to an unwanted state). If we wrap the two functions into two atomic blocks then the third behaviour would be automatically ruled out by the system as one of the two transactions will be aborted depending on the implemented policy.

2 Syntax

In this section we give the syntax of our calculus. Let us assume the existence of the mutually disjoint sets \(\mathcal {V}\) (a set of variables) and \(\mathcal {I}\) (a set of transaction identifiers), ranged over by xyz and th, respectively.

The syntax of the calculus is given in Fig. 1. Write and read access to the variable x are represented with actions \(\mathtt {wr} ({x})\) and \(\mathtt {rd} ({x})\). The sequential execution of the actions \(\mathtt {wr} ({x})\) and \(\mathtt {rd} ({x})\) together with the choice operator \(+\) build the processes, given with AB productions. The term represents a transaction, where t is a unique identifier, A is the body of the transaction and \(\varGamma \) is the set recording the identifiers of the transactions which have the write access to the variable that transaction t has to read. The idea behind the set \(\varGamma \) is to allow transaction t to have read access to any variable, but to record the write access to them. In this way if the transaction that writes on the variable fails, the transaction that reads the same variable has to fail too. More explanations will be given in Sect. 3.

Transactions, together with processes, build expressions. An expression can be prefixed with the actions \(\mathtt {wr} ({x})\) and \(\mathtt {rd} ({x})\) and we denote it as \(\alpha .X\). Two expressions X and Y can be executed in parallel, \(X\,|\,Y\), or in sequential order XY. We can note that the expression X can be the process that is not inside of the transaction, and that operation ;  allows us to have a transaction followed by an action (for example ). The whole system, called configuration, is denoted with C and it represents the expressions together with the shared memory. The shared memory M is made of triples of the form \(\langle {x,W,R}\rangle \) for every variable in the system. In \(\langle {x,W,R}\rangle \), x is the variable name, W and R are the sets recording transactions which had write and read access to x, respectively. Let us note that we abstract away from the value contained by variables, since this is not relevant for our framework. We just need to record whether a variable is read (a transaction reads its value) or modified (a transaction changes its value).

Fig. 1.
figure 1

Syntax

In order to write expressions in a more compact way, we define the notion of history context. For instance, having a transaction we can write it as where \(\mathtt {H}=\mathtt {wr} ({x}).\bullet +B\). Formally:

Definition 1 (History context)

A history context \(\mathtt {H}\) is a process with a hole \(\bullet \), defined by the following grammar: \(\mathtt {H}::=\bullet \,|\,\alpha .\bullet +A\).

3 Semantics

The semantics of our calculus is presented in two steps. First, we give the basic rules of the framework (common to all the policies) and then, we present the extra rules, necessary to model reader or writer preference. With reader preference, we intend that reading the value of a variable is always possible, i.e. no read access should be suspended, unless the write access already took place. Writer preference, on the other side, allows the write access to the value of a variable x even if some read access already took place. In this case, all the executing transactions with the read access to a value x need to be aborted and brought back to their initial state.

In what follows we provide the auxiliary functions necessary for the semantics of the calculus: the function which computes the set of the transaction identifiers of a given expression and the operation which removes transaction identifiers from the system.

Definition 2 (Set of the transaction identifiers)

The set of the transaction identifiers of a given expression X, written \(\mathtt {id}(X)\), is inductively defined as:

Definition 3 (Removing of identifiers)

The operation of deleting transaction identifier t from the configuration C, denoted as \(C_{@t}\), is defined as follows:

When a transaction fails, the effects of the internal computation are undone and the entire transaction is restarted, that is, brought back to its initial state. As a consequence, the transactions depending on it are also rolled back. Dependency between transactions changes with the chosen preference. We shall see more information about the preferences by the end of this section.

To be able to identify the state of the internal computation of a transaction, we mark it with symbol \(^{{\wedge }}\). For instance, if we consider transaction , the actions \(\mathtt {rd} ({x})\) and \(\mathtt {rd} ({y})\) represent the past of the transaction and the action \(\mathtt {wr} ({z})\) is the next action to be executed.

Now we define our rollback operator which brings a transaction back to its initial state i.e. the symbol \(^{{\wedge }}\) is placed in the beginning of the transaction and its set \(\varGamma \) is empty. For instance, if we roll back transaction , we obtain . Formally, we have:

Definition 4 (Rollback operator)

The rollback operator on the transaction , written \(\mathbf {roll}(t)\), is defined as: .

In what follows, we give the semantics of our calculus. We shall start by introducing semantics rules representing the base of our framework (rules that are common for both models) and then we show the additional rules for each preference.

Fig. 2.
figure 2

Common rules for both models

The common rules are given in Fig. 2. An action executed outside a transaction can be seen as an atomic step in which the action is discarded after the execution (rules \(\textsc {WriteP}\) and \(\textsc {ReadP}\)). Therefore, there is no need to keep track of its access to the variable. The only constraint is that they cannot access the variable while some transaction has read or write access to it.

Rule Write describes when a transaction can modify the content of a variable. To do so, there should not be another transaction which has already accessed the variable in either writing or reading mode. After the execution, the identifier t is added to the write access set W of the variable x and the symbol \(^{{\wedge }}\) is moved to the next computational step. Rule \(\textsc {Read}\) allows the transaction t to execute the action \(\mathtt {rd} ({x})\) at any moment. Then the identifier t is added to the set R of the variable x and the set \(W\setminus t\) is added to the set \(\varGamma \) (if write and read access to the variable x are in the same transaction t, then it is not necessary to save the identifier t into a set \(\varGamma \)).

To have a better intuition about these two rules, we give a simple example. Consider the transaction t with a corresponding shared memory

After executing the write access to the variable x, we obtain the system

where the pointer \(^{{\wedge }}\) is moved to the next action and the identifier t is added to the write set of the variable x. Now we can perform the read access to variable y and we have:

where the identifier t is added to the read set of the variable y. The set \(\varGamma \) of the transaction t remains empty since there is no transaction which had write access to variable y.

Rule \(\textsc {Par}\) allows expressions to execute in parallel (in an interleaving fashion) ensuring the uniqueness of the identifier t. By executing its last action, the transaction t can commit if the set \(\varGamma \) is empty, by applying the rule \(\textsc {Commit}\). After it commits, the execution proceeds with the continuation Y and the identifier t is deleted from the remaining system. The intuition is that transaction t can commit if the other transactions, having a write access to the variables that transaction t read, have been committed. The rollback of the transaction t can be done with the rule \(\textsc {RollR}\). It will force every transaction in parallel having the identifier t in their set \(\varGamma \) to roll back too. The intuition is that when the transaction with \(\mathtt {wr} ({x})\) rolls back, every transaction which has read access to x should roll back as well. For instance, let us consider the system containing following transactions:

and that transaction t needs to be rolled back. Then, by applying the rule \(\textsc {RollR}\), we obtain the system:

in which transaction \(t_1\) is rolled back too since \(t\in \varGamma _1\) while \(t_2\) remains the same.

Now we can give the rules necessary to model reader and writer preference. To give a better intuition about the differences between the two models, we use the example from the introduction as a running example.

Reader Preference. To model the reader preference we use the rules from Fig. 2 and the rule given below.

figure b

The rollback operator is triggered when the transaction t cannot write on the variable x (this happens when \(W\not \subseteq \{ t\}\) or \( R\not \subseteq \{ t\}\)). With the rule the transaction t goes to the state \(\mathbf {roll}(t)\), i.e. the initial state of the transaction. Additionally, the identifier t is removed from every triple of the shared memory.

To illustrate it, we use the example from the introduction, abstracting away from the read and write values contained in variables and representing accesses of two threads to the shared memory in our framework with transactions \(t_1\) and \(t_2\). Transaction \(t_1\) has read accesses to variables y and x and then writes on variable z, while \(t_2\) has read access to variables z and then writes on x. We have the following system

We assume that read accesses are executed in parallel and the obtained system is

Now transaction \(t_1\) is executing write access to variable z but since in the memory for variable z we have \(R\not \subseteq \{ t_1\}\) (\(R=\{t_2\}\)), the transaction \(t_1\) needs to roll back according to the rule , and we have

where .

Writer Preference. To model the writer preference we use the rules from Fig. 2 and the rules given below.

figure c

The rollback is triggered by the writer only in the case when another transaction has write access to the same variable. Therefore the condition on the rule W-RollW is simply \(W\not \subseteq \{ t\}\). The additional rule, with respect to the reader preference is the rule . It allows a transaction to modify the value of a variable x if other transactions have read access to it. At the same time, all transactions executing in parallel whose identifiers belong to the set R, are requested to roll back.

To illustrate it, we use the same example as for the reader preference where read accesses are executed already. Therefore, we have the system

Now we can execute the write access to variable z, since in the rule W-Pref the condition for the read set R allows a transaction to perform the write access, and in that case all transactions in parallel having read access to variable z need to be rolled back. Therefore, transaction \(t_1\) executes write access, while \(t_2\) will be rolled back, and we have:

where .

4 Conclusion and Future Work

We have presented a framework to express the STM mechanism in a simple shared memory context. The framework is able to model two different policies for the execution of the concurrent transactions: writer and reader preference. Our intention is to start from a simple calculus and then to add in a modular way: nested transactions, data structures (e.g., C structures) and more complex scheduling policies. Nested transactions will require to record for each transaction a list of its children transactions. These children inherit the access of the parent transaction. There exist different policies to deal with nested transactions: closed nested transactions [17] and open nested transactions [18]. The difference is that in the first case the parent does not execute till all the children have committed, while in the second case the parent can commit even before its children. This may lead to inconsistencies which have to be dealt with compensations.

Our ultimate goal is then to prove that the modular framework satisfies the opacity  [14] property, that is, all the execution traces of our semantics, where the transactional bodies are interleaved, are equivalent to executions in which transactional blocks are executed as a whole (in a lock-based fashion) without being interleaved with other transactions.