As promised yesterday, I’d like to show off a few bits of my STM library. Of course it’s far from done, and is still missing several key features, but the core library is in pretty good shape. So as they say on the internets, “my STM library, let me show you it”
In the following, I’ll show a slightly modified version of one of my test cases. It shows how to call and use my library, from the user’s point of view. The test spawns a number of threads, which each wait on a barrier (because if one thread was allowed to run while another was being constructed, I’d get fewer concurrent transactions, and my test would be less likely to uncover race conditions), and then perform a fixed number of transactions. In each transaction, two transactional variables are opened for writing, one is decremented and the other incremented. If the sum of these variables is nonzero in any iteration, the thread registers a failure.
And if, after all the threads have terminated, the value of each of these variables is not what was expected, a failure is registered as well.
So from a testing point of view, there should be plenty of opportunity for things to go wrong. Just a single small race condition somewhere, and one of the many millions of reads would be inconsistent and the test would fail.
#include <stm.hpp> // my STM library
#include <boost/test/unit_test.hpp> // Boost.Test is used to supply a unit-testing framework
#include <boost/thread.hpp> // Boost.Thread is used as a threading API
// define the number of threads to run, and the number of iterations for each
enum { thread_count = 8, iterations = 200000 };
// The following are transactional variables. The shared template ensures that the contained value
// can only be accessed as part of a transaction, and provides the necessary metadata
// for checking validity and consistency
// Two such integers are created, both initialized to zero
stm::shared<int> val1(0);
stm::shared<int> val2(0);
BOOST_AUTO_TEST_SUITE( threads ) // define a test suite named "threads"
// this function defines the body of our transaction.
// We're passed a transaction, which can be used to open any "shared" variables
bool tx_func(stm::transaction& tx){
// open both variables for writing
int& a = val1.open_rw(tx);
int& b = val2.open_rw(tx);
// modify the variables freely
--a;
++b;
return a + b == 0; // Our transaction returns a bool. Other return types (or void) are also supported
}
// this class defines a thread. operator() is called as the thread's entry point
struct thread_functor{
// In the constructor, the thread object is given a barrier it can synchronize on,
// and a reference where it can write the its result (success/failure)
thread_functor(boost::barrier& bar, int& res) : bar(bar), res(res) {}
void operator()(){
// when the thread is first created, we wait for the barrier
// This ensures that no transactions are running until all threads have been constructed
bar.wait();
for (int i = 0; i < iterations; ++i){
// for each iteration, pass our transaction function to the "atomic" function, which executes it atomically.
// To get a non-void return type, we have to specify the template parameter explicitly
// (this can be avoided in C++0x using the return_of template to deduce the return type implicitly)
// depending on the return value of the transaction, write success or failure back
res = (res != 0) && stm::atomic<bool>(tx_func) ? 1 : 0;
}
}
boost::barrier& bar;
int& res;
};
// another transaction, to be executed after our helper threads terminate,
// to verify that the right number of modifications have occurred
// note that here variables are opened for reading only
void verify(stm::transaction& tx) {
const int& a = val1.open_r(tx);
const int& b = val2.open_r(tx);
BOOST_CHECK_EQUAL(-a, thread_count * iterations);
BOOST_CHECK_EQUAL(b, thread_count * iterations);
}
// finally, we get to our test case itself
BOOST_AUTO_TEST_CASE ( short_concurrent_transactions )
{
boost::barrier bar(thread_count);
boost::thread_group gr;
int res[thread_count]; // array of results
// set all the results to an initial true/1 value (since each iteration "and"'s it together with the current result
std::fill(res, res+thread_count, 1);
for (int i = 0; i < thread_count; ++i){
gr.create_thread(thread_functor(bar, res[i])); // create the threads, passing the necessary parameters to each
}
gr.join_all(); // wait for all threads to terminate
// verify that each thread return success
for (int i = 0; i < thread_count; ++i){
BOOST_CHECK_EQUAL(res[i], 1);
}
// run a final transaction to access both variables and check their final values
stm::atomic(verify);
}
BOOST_AUTO_TEST_SUITE_END()
In the above, I used a function object to represent threads, and a regular function to represent transactions. Of course in both cases, either would work — a function object would potentially be more efficient as it is easier for the compiler to inline, but I used a function for brevity.
In C++0x, of course, lambdas could also have been used in both cases.
One of my design goals has been to make basic usage as simple and intuitive as possible, and I think I’ve succeeded so far. Any C++ programmer who is familiar with the STL algorithms or the Boost libraries, should find my library’s interface very straightforward. Note especially that all the transaction “magic”, of verifying validity and retrying transactions as needed, is completely invisible to the user. You simply define a function expressing what your transaction should do, and pass it to the atomic function.
In its current version, this test is able to execute around 1,800,000 transactions per second on my Core Duo 2GHz laptop. (Of course, with transactions as small as these, opening only two variables each, performance is a lot better than it would be in real-world transactions.
So that’s it for now. Of course I’ve got a few more user-facing features in the pipeline1, and a lot of backend changes, but the basic functionality is there, and I’m pretty happy with it so far.
-
It should probably be possible to specify that the transaction should not automatically retry if it fails to commit, and instead abort with an exception. It should also be possible to define nested transactions, and use operations such as OrElse and Retry primitives introduced in Haskell STM ↩
Tags: c++, stm, thesis, transactional-memory





Looks promising… i like how current c++ libraries and utilities tend to take more and more work as well as error sources from the user — for me that is the main value of modern c++ paradigms.
Yeah, definitely. C++ has a lot of shortcomings, but this is one of its saving graces. I can’t think of another mainstream language that makes that possible