Software Transactional Memory IV - Thread-Bound Transactions

I´ve explained in my previous posting, how a single transaction weaves its magic of isolating changes to transactional objects (txo) and atomically making them visible on commit. But what´s the "reach" or "scope" of a NSTM transaction? How many transaction can be open at the same time?

Transactions are kept in TLS

To answer these questions it´s vital to understand where transactions are stored: in thread local storage (TLS). NSTM transactions are bound to a single thread, the thread they were created on. This is different from System.Transactions which are thread-independent (see [1] for details). But binding NSTM transactions to a thread is on purpose. NSTM is supposed to make multithreaded programming easier by isolating the work done in parallel on shared in-memory data structures. Thus transactions need to thread-local to keep changes made to txos in local transaction logs (txlog).

Application code in each thread works with its own transactions which of course can work on the same txos:

image

A txo is just briefly locked during access to avoid inconsistencies during single reads/writes. Since a txo is not just a singel value but containes at least a value and a version number which must not get out of step that´s necessary. But this kind of locking is hidden from you. Don´t worry about it. Nothing is permanently locking for the duration of a transaction. No deadlocks can occur. Rather NSTM bets on optimistic locking as already explained: txos are validated during commit (at latest) to check if they were changed by some other transaction and if so, the transaction fails. It´s the same as with ADO.NET when saving changes made to a DataSet.

When you call Read() or Write() on a INstmObject the object retrieves the current NSTM transaction from TLS and delegates further processing of your request to the transaction.

The usual transaction scopes

TLS does not just point to a single transaction, though. Rather it contains a stack of transactions (txstack) because you can nest them. Check out this code:

    1 using (INstmTransaction txOuter = NstmMemory.BeginTransaction())

    2 {

    3     ...

    4     using (INstmTransaction txInner = NstmMemory.BeginTransaction(NstmTransactionScopeOption.RequiresNew, ...))

    5     {

    6         ...

    7     }

    8     ...

    9 }

In line 3 there is one transaction on the TLS txstack, in line 6 there are two on txstack with txInner being the topmost. In line 8 it´s again just one transaction: txOuter.

image

Each transaction of course keeps its own transaction log. By default they are independent. However, they need to be ended in reverse order despite their independence. A transaction opened while another was active needs to be committed or rolled back first. That´s why you should nest transactions like above with using statements. They ensure the right order.

But there are several transaction scope options. Above the inner transaction is opened with RequiresNew. This ensures a new transaction is opened although another one is already active. The default however is just Required:

   10 using (INstmTransaction txOuter = NstmMemory.BeginTransaction())

   11 {

   12     ...

   13     using (INstmTransaction txInner = NstmMemory.BeginTransaction())

   14     {

   15         ...

   16     }

   17     ...

   18 }

The inner transaction is opened with the default scope Required which means, a new transaction is only created if none is active yet. But in line 13 there is already txOuter active so no new transaction is started and txInner equals txOuter. In line 15 there´s still only one transaction on the txstack. This mirrors what EnterpriseServices or System.Transactions have offered all the time.

Truely nested transactions

NSTM goes beyond that, though, by providing for real nested transactions:

  100 using (INstmTransaction txOuter = NstmMemory.BeginTransaction())

  101 {

  102     ...

  103     using (INstmTransaction txInner = NstmMemory.BeginTransaction(NstmTransactionScopeOption.RequiresNested, ...))

  104     {

  105         ...

  106     }

  107     ...

  108 }

Using RequiresNested not only ensures there is a new transaction active in line 105, but also that any changes committed with txInner can be undone by rolling back txOuter! In the first sample above txOuter and txInner were independent. Whatever txInner committed txOuter could not undo. Although syntactically nested the transactions had no more to do with each other than transactions on different threads. But nested transactions are different: When txInner is started in line 103 is clones the txlog of txOuter and thus sees all changes already made to txos. And when txInner it commits, it does not write any changes to the txos, but just to its parent transaction txOuter. That means, changes of inner transactions are not visible "to the public" until its parent transactions "reaffirms" them by also committing.

Finally there is the RequiredOrNested scope option. If there is already a transaction running no new one is created, as long the options (scope, isolation level, clone mode) of this transaction are "compatible" with the options stated for the new transaction. If the existing transaction´s options are less strict, though, then a nested transaction is created. This option seems useful for library developers who don´t want to necessarily open new transactions for whatever their library code does - but also don´t want to sacrifice transactional strictness. Consider this scope option an experiment and food for thought ;-)

Summary

See, there answers to the initial questions were easy: A transactions scope is the thread. And you can have as many transactions open as you like. They just cannot overlap, but the can truely be nested. Threadsafety is guaranteed for transactions as a whole and INstmObject transactional objects. If you use CloneOnRead you´re pretty much on the safe side also for the values of txos, though. But this depends on how deep your clones are. For scalar types and value types containing just scalars you don´t have to worry. Deep object hierarchies, though, are not locked in any way! Take this into account when you implement ICloneable. Nevertheless NSTM is a very convenient and efficient way to parallelize work.

What´s next?

You know almost everything about my Software Transactional Memory implementation. There´s only the integration with System.Transactions missing - but not for long ;-)

Resources

[1] Juval Löwy, Can´t Commit: Volatile Resource Managers in .NET Bring Transactions to the Common Type, http://msdn.microsoft.com/msdnmag/issues/05/12/transactions/default.aspx

No Comments