The DikuSTM library
For my Masters Thesis at University, I designed and developed a Transactional Memory1 library in C++. I named it DikuSTM, after the Computer Science department where I studied, which is semi-formally nicknamed DIKU. At the end of the project, in the spring 2010, I had a basically functional prototype, although the implementation of some parts was suboptimal, and I later discovered a few bugs and race conditions. This old version of the library, along with my Masters Thesis itself, is available here.
Afterwards, of course, I took a long break from the project. But a few months ago, I decided to resume work on the project in my (somewhat limited) spare time, and while a few areas can still be improved, and the documentation is nearly nonexistent, the library is actually nearing a useful state.
On this page, I intend to track the status of the library, as well as whatever documentation and information I have written.
A reasonably up-to-date snapshot of the code can be found here. Or you can pull it from the bzr repository here
While no detailed and up-to-date documentation exists yet, I have written the following short guide to illustrate the basic usage of the library, and hopefully allow anyone who’s interested to take the library for a test drive.
10-minute Whirlwind Tour
Transactional Memory can be implemented in a variety of different ways, each with their advantages and disadvantages. But at its core is the notion that a block of code can be annotated to behave as a transaction. Just like their database equivalents, a STM transaction guarantees that whatever occurs within it happens Atomically and in Isolation, while keeping the application state Consistent at all times. (In the database world, Durability is guaranteed as well, and the four properties are known as the ACID properties, but as STM operates in memory, it is inherently volatile, and durability is both impossible to achieve, and unnecessary — the rest of the application state is lost on a crash anyway, so preserving the state of in-memory transactions would be useless, even if it was possible.
When a transaction ends, it is attempted committed, at which point, the system verifies the consistency of the data touched by the transaction, and if everything is consistent, the transaction is allowed to commit, atomically making all its changes visible to the rest of the system. If an inconsistency is detected, typically because another transaction has committed a modification to an object read by the currently committing transaction, that transaction has to be aborted and rolled back, so that none of its changes are visible. The transaction then implicitly retries, starting over and performing all its modifications again, before trying to commit once more.
A consequence of this execution model is that the body of a transaction may be evaluated more than once — and so, side effects and I/O in particular is, in the general case, forbidden within a transaction. Side effects are permissible if they can be rolled back along with the transaction, but I/O, such as writing to standard output would lead to surprising results if done within a transaction that is forced to roll back and retry.
Under the hood, these transactions can be implemented using traditional synchronization mechanisms, and by copying data into a private buffer accessed only by a specific transaction. A transaction appears to execute in isolation if it can create private copies of any shared data it modifies — regardless of what the rest of the system may be doing, these private copies will stay untouched, and so the transaction can proceed as if it were alone on the system. Atomicity can be achieved by defining a suitable locking protocol, which allows transactions to lock shared objects while creating the aforementioned private copy, and later on, when the transaction commits and applies its changes to the global application state. And consistency can be ensured by tagging every shared object with a version number or a timestamp, so that for any given transaction, we can record the timestamp at which it starts, and throughout the transaction’s lifetime, verify that objects accessed by it have not been modified by other objects since the transaction started.
That’s the elevator pitch describing the fundamental idea behind Software Transactional Memory, and a few hints at how such a library might work under the hood. Of course, there are many different implementation strategies, but the above should be good enough to give a high level understanding of what is going on in a STM library.
Using the DikuSTM Library
The simplest way to experiment with my library is to treat it as a header-only library. A few .cpp files do exist in my code, and are used if the preprocessor symbol STM_HEADER_ONLY is not defined. This is currently defined in file config.hpp, along with a number of other settings and macros. In short, the library can be used as-is simply by #include’ing the appropriate header, stm.hpp, located in the root of the downloaded archive. The Boost.Thread library is used internally in the library, and so it may be necessary to link to this library as well. The internals of the library are located in the stm directory. If you go looking at the tests located in the other directories, be warned that they have accumulated over the last 18 months, since I started work on the project, and so, they’re ugly, inconsistent and even use two different test frameworks. The old ones use Boost.Tests, and the new ones use the excellent, and highly recommended, Catch library.
Now, getting back to the interesting part, the library itself, a simple STM Hello World could look like this:
#include <stm.hpp>
#include <iostream>
std::string my_transaction(stm::transaction& tx) {
return "Hello world";
}
int main() {
std::cout << stm::atomic<std::string>(my_transaction);
}
Of course, this example runs in a single thread, and as such, we have no need for transactional memory whatsoever. But it illustrates the basic operation of defining a transaction and invoking it atomically.
This simple (and pointless) snippet highlights a few interesting features of the library:
- transactions are defined as functions or functors —
stm::atomicaccepts any “function-like” object, whether it is a plain function pointer as in the example above, or a function object, or, in C++0x, a lambda expression. stm::atomicis where the magic happens. It executes the function passed to it as a transaction, atomically, consistently and in isolation. It is currently necessary to specify the return type as a template parameter (callingstm::atomic()with no template parameters implies that the transaction returnsvoid. In C++0x, the return type could be deduced automatically through the magic ofdecltype, but the library does not yet take advantage of that feature.)- the transaction takes a reference to an object of type
stm::transactionas its sole parameter (and so, in order to pass parameters to the transaction, the transaction must be defined as a functor or function object, so that the user-defined parameters can be passed to its constructor). Thestm::transactionobject acts as the interface for communication with the STM library. If the user wishes to abort the transaction, for example, thestm::transactionobject contains a member functionabort(). - for the common case, the library just does the right thing. We didn’t need to call an explicit
commit()function — the transaction is implicitly committed when control leaves the scope of the transaction function. Validation is also taken care of for us. During commit, inconsistencies are automatically detected, the transaction is rolled back and retried, and so, no error handling is necessary on the user’s part. In this case, it means that we can simply execute the transaction throughstm::atomic, and stream the result tostd::cout, without having to worry about “what happens in the case of a conflict, and if the transaction fails to commit”. The user can simply assume that the transaction will commit successfully, and return the desired value. It may internally require a few tries to commit successfully, but the user of the library does not see this, or need to write code to handle it.
This was a rather pointless example, however. We created a transaction, but we had no threads, and no shared data. So, let us add some shared data.
In the “ideal” implementation of transactional memory, everything happens inside a transaction, and all data is modified transactionally. However, such an implementation would be impossible to implement efficiently in C++ (and would be hard to integrate into existing code), and so only objects wrapped in a special stm::shared template wrapper are treated “transactionally”; modifications to such objects occur in isolation, and are committed atomically.
So, here is a simple example which accesses and modifies some shared data:
#include <stm.hpp>
#include <iostream>
stm::shared<std::string> greeting("Hello");
struct combine_string {
void operator()(stm::transaction& tx) {
std::string& str = greeting.open_rw(tx);
mystr += " World";
}
};
std::string get_string(stm::transaction& tx) {
return greeting.open_r(tx);
}
int main() {
stm::atomic(combine_string());
std::cout << stm::atomic<std::string>(get_string);
}
Again, hopefully fairly simple code. And a couple of observations:
- in order to access a shared object, we need to open it, essentially registering with the STM system that the specified transaction is now accessing the object. Objects can be opened in “read-only” mode with
open_r, which returns a const reference, or in “read-write” mode withopen_rw, returning a reference. - once opened, we get a plain reference to the internal type, and can deal with the object stored within the
sharedwrapper completely as we would expect to. We also see that the API is non-intrusive, so that existing types can be used as-is. - access to an opened object is automatically “closed” again once the transaction terminates. No explicit
close()call is necessary (or possible). - in order to open an object, we have to pass in our
stm::transactionreference. This acts as a “key”, indicating that yes, we are working inside a transaction, and so it is safe to open the object. Opening a shared object outside a transaction would be dangerous, and by requiring a reference to the current transaction, we prevent the user from accidentally attempting to do this.
We still have only a single thread, so once again, the example is somewhat pointless. But if we had had more than one thread, then they could all have opened the greeting string within their respective transactions, and every transaction would be guaranteed to see a consistent and valid value. The “intermediate” invalid state while the string was being appended to occurred in isolation within the modifying transaction, so that other transactions will only see the initial or the final value, regardless of when the transaction attempts to open the object.
But let’s do something realistic now. Let’s add a second thread (I’m going to use the Boost.Thread API, but any threading API will work — the library makes no assumptions about how the thread was created):
#include <stm.hpp>
#include <iostream>
#include <boost/thread.hpp>
stm::shared<std::string> greeting("Hello");
stm::shared<bool> has_name(false);
struct add_name_tx {
add_name_tx(const std::string& name) : name(name) {}
void operator()(stm::transaction& tx) {
if (has_name.open_rw(tx)) {
clear_name();
}
bool& b = has_name.open_rw(tx);
b = true;
std::string& str = greeting.open_rw(tx);
mystr += " " + name;
}
private:
std::string name;
};
struct add_name {
add_name(const std::string& name) : name(name) {}
void operator()() {
stm::atomic(add_name_tx(name));
}
private:
std::string name;
};
void clear_name_tx(stm::transaction& tx) {
greeting.open_rw(tx) = false;
greeting.open_rw(tx) = "Hello";
}
void clear_name() {
stm::atomic(clear_name_tx);
}
std::string print_name_tx() {
return greeting.open_r() + (has_name.open_r()) ? ". You seem like a nice person." : ". Who are you again?";
}
void print_name() {
std::cout << stm::atomic<std::string>(print_name_tx);
}
int main() {
boost::thread_group group;
group.create_thread(add_name("Jesper"));
group.create_thread(clear_name));
group.create_thread(print_greeting);
group.join_all();
}
A bit more complex, but hopefully it still makes sense.We create three threads; one which appends a name to the greeting string (clearing the existing name if one existed), another which clears the name, and one which prints the greeting string, along with a small extra message depending on whether or not a name has been added to the string. We also have two shared objects now, which must be updated atomically. When a name is appended to the greetings string, the value of has_name must be true, and vice versa. Our program logic relies on these objects being in sync at all times. And with traditional lock-based code, this would have been quite brittle. It’d be easy enough to add the necessary locks to ensure that as the code is today, these values are always updated together. But every future extension to the program would have had to keep this in mind as well. It’d be up to the developer to remember to “never access one of these two values outside of a critical section”.
But with transactions, it just works. We want to add a name to the string? Just open a transaction in which both are modified. The modification depends on the current value of has_name? Well, just open it and check its value. It is guaranteed to stay consistent throughout the transaction. And if we try to append a name, and one already exists? Why, we invoke the clear-name function which, itself, also launches a transaction. That’s no problem. With locks this would have forced us to consider whether our locks are reentrant, whether we’re introducing any deadlocks, and so on. But we can compose transactions just fine. And I can’t simply forget myself and try to access one or both shared objects outside a transaction. Because if I don’t have a stm::transaction reference to pass to the open function, I can’t get to the object stored within the shared wrapper. With this code I can’t tell you which message is printed out, as it depends on the precise timing of the three threads and I do nothing to sequence or synchronize the threads in any particular order, but I can guarantee that whatever is printed will be valid, and it will be either the message in which a name is specified, or the message without a name. There are no deadlocks or race conditions, at least not of the dangerous kind. I don’t know what the program’s output will be precisely, but I know that it will be one of two messages, both of which are considered valid. The program does depend on the precise ordering of the threads, but its state is guaranteed to be consistent and valid at all times.
Of course, this example is still quite limited. It allows us to stop worrying about atomicity and thread-safety, but most programs will also need some way to order and synchronize code, so that they can ensure that a specific operation on thread A only ever happens once another operation has been completed on threads B, C or D. You can use conventional lock-based primitives for this if you like, but the STM library also provides a few tools you may use. A few other features should also be mentioned: like the ability to return a consistent snapshot of multiple shared objects from a single transaction, or the ability to manually abort transactions, or allow the system to fall back to attempting a different transaction if the first one fails to commit. However, these will be described some other time.
Hopefully this quick tour of the most basic usage of my library has given you a reasonable idea of “what it’s all about”. Feel free to poke around at the code, and try using my library. But be aware that it is still not quite production-quality. The code needs to be cleaned up, a few aspects need to be optimized, the file organization is a mess, and the documentation is… well, if you’ve got this far, you’ve seen all the documentation that currently exists. But to the best of my knowledge, the library works.
Blog posts
While I wrote my Masters Thesis, I occasionally blogged about the status of the project. These posts can be found in the following list:
Thesis, yay!
Software Transactional Memory
Being functional in an imperative language
Using My STM Library
Houston, we have a (performance) problem
Empty statement of intent
-
More accurately, a Software Transactional Memory library — Transactional Memory was originally envisioned as a hardware feature, but later research has investigated software implementations as well, and the two variants have become known as HTM and STM respectively. ↩