The API for my .NET Software Transactional Memory (NSTM) I´ve described so far is straightforward, I´d say. It´s close to what you´re used to from transactional dabase access and it´s even integrated with System.Transactions: open a transaction, do some stuff with transactional objects, commit the transaction. All very explicit.
Despite its familiarity this explicitness kind of stands in the way of what a piece of object oriented code is supposed to accomplish. It´s something you´ve to concern yourself with although it does not add to the raw functionality you want to achieve. And by being imperative it´s prone be done incorrectly. A more declarative way to do in-memory transactions and less explicitness thus would be nice now that objects have become transactional and automatically threadsafe in general.
I thought about that from the being when I started to work on NSTM. Microsofts STM implementation SXM shows, how transactionality could be made more declarative and "invisible": With SXM you just adorn a class with the attribute [Atomic] to make its instances transactional. That´s cool - but so far I did not consider it for NSTM, since it seemed to require fiddling around with proxies. That´s why with SXM you need to instanciate transactional objects through a factory which you´ve to create for each transactional class:
1 [Atomic]
2 public class Node
3 {
4 private int value;
5 private Node next;
6 ...
7 }
8 ...
9 XObjectFactory factory = new XObjectFactory(typeof(Node));
10 Node node = (Node)factory.Create(value);
The benefits of using an attribute to make classes transactional to me seem to be lost through this cumbersome instanciation process. That´s why I did not go down this road until now.
PostSharp to the rescue
But then one day - lo and behold! - a very, very cool tool was revealed to me by InfoQ (which I´ve come to like as much as theserverside.net):
PostSharp from Gael Fraiteur is a godsend! It´s one of those technologies you´ve waited for all along without really knowing. It´s a technology that will change how you approach software development. At least that´s what his done for me since I first read about it.
What PostSharp does is not new, though. It´s a kind of Aspect Oriented Programming (AOP) weaver. And it´s a tool to alter the MSIL code in assemblies. But there are already several APO frameworks out there - also for .NET -, so why another one? There are already tools to alter assemblies like Phoenix from Microsoft and Cecil from the Mono project.
What makes PostSharp so remarkable to me, though, is in which way it combines AOP with MSIL modification. It makes it so dead easy!
- You´re not forced to learn MSIL.
- The weaving is done automagically for you by an invisible post build step.
- No factories needed to instanciate classes annotated with aspects, because no proxies are used.
- For many common types of aspects (e.g. pre/post processing of method calls, field access interception) there are templates that help you get your own aspects up and running in just a few minutes.
Congratulations Gael! And thanks for being so responsive to questions and bug reports!
Aspect oriented transactions
The first thing I tried with PostSharp to make NSTM transactions declarative. This seemed to be easy to do and would result in an also familiar transactional programming model. COM+/EnterpriseServices, ASMX, and not WCF all sport some attribute to make methods transactional. Here´s an excerpt from Juval Löwy´s article [1] on that topic:
1 [ServiceContract]
2 interface IMyContract
3 {
4 [OperationContract]
5 [TransactionFlow(TransactionFlowOption.Mandatory)]
6 void MyMethod(...);
7 }
8
9 class MyService : IMyContract
10 {
11 [OperationBehavior(TransactionScopeRequired = true)]
12 public void MyMethod(...)
13 {
14 ...
17 }
18 }
MyMethod in the service contract is declared as to be wrapped in a mandatory transaction. The service implementation does not have to do anything imperatively for that. The WCF infrastructure will create a new System.Transactions transaction if necessary behind the scene.
That´s what I wanted for NSTM, too, and what now seemed within reach through PostSharp. And indeed it was very easy to implement. Here´s my transactional aspect whipped up in some 30 minutes after downloading PostSharp:
1 [Serializable]
2 [AttributeUsage(AttributeTargets.Method |
3 AttributeTargets.Property |
4 AttributeTargets.Constructor)]
5 public class NstmAtomicAttribute : OnMethodBoundaryAspect
6 {
7 private NstmTransactionScopeOption transactionScope;
8 private NstmTransactionIsolationLevel isolationLevel;
9 private NstmTransactionCloneMode cloneMode;
10 private bool createdNewTx;
...
35 public override void OnEntry(MethodExecutionEventArgs eventArgs)
36 {
37 NstmMemory.BeginTransaction(this.transactionScope,
38 this.isolationLevel,
39 this.cloneMode,
40 out this.createdNewTx);
41 }
42
43 public override void OnException(MethodExecutionEventArgs eventArgs)
44 {
45 if (this.createdNewTx) NstmMemory.Current.Rollback();
46 }
47
48 // will always be called, also after OnException()
49 public override void OnExit(MethodExecutionEventArgs eventArgs)
50 {
51 // only commit, if no exception has been thrown
52 if (eventArgs.Exception == null && this.createdNewTx)
53 NstmMemory.Current.Commit();
54 }
55 }
How does it work? First have a look at some code which uses this attribute:
1 static INstmObject<int> myAccount, yourAccount;
2
3 static void Main()
4 {
5 myAccount = NstmMemory.CreateObject<int>(1000);
6 yourAccount = NstmMemory.CreateObject<int>(500);
7
8 try
9 {
10 TransferMoney(350);
11 }
12 catch (Exception ex)
13 {
14 Console.WriteLine("*** {0}", ex.Message);
15 }
16
17 Console.WriteLine("my account: {0}", myAccount.Read());
18 Console.WriteLine("your account: {0}", yourAccount.Read());
19 }
20
21 [NstmAtomic()]
22 static void TransferMoney(int amountToTransfer)
23 {
24 myAccount.Write(myAccount.Read() - amountToTransfer);
25 yourAccount.Write(yourAccount.Read() + amountToTransfer);
26 throw new ApplicationException("Money transaction failed!");
27 }
The method TransferMoney() does not open explicitly a transaction. Rather this is done by the NSTM infrastructure because the method is adorned with the [NstmAtomic()] attribute. The default then is to commit the transaction when the method returns; but it´s aborted if the method throws an unhandled exception.
Of course, if you like, you can pass a transaction scope or isolationlevel etc. to the attribute to influence how the transaction is created. By default the scope is Required so nested calls don´t each created new transactions.
Now, how is this magic woven (sic!) by PostSharp? I won´t explain PostSharp in depth here, but a quick glimpse behind the curtain won´t hurt, wouldn´t it ;-)
Remember, PostSharp modifies the original assembly in a post build step like an O/R Mapper´s enhancer (e.g. Vanatec OpenAccess) or an obfuscator (e.g. Xenocode). So here´s the "enhanced" TransferMoney() method as seen through Reflector:
private static void TransferMoney(int amountToTransfer)
{
MethodExecutionEventArgs args;
try
{
object[] arguments = new object[] { amountToTransfer };
args = new MethodExecutionEventArgs(methodof(Program.TransferMoney, Program), null, arguments);
~PostSharp~Laos~Implementation.~aspect~1.OnEntry(args);
if (args.FlowBehavior != FlowBehavior.Return)
{
myAccount.Write(myAccount.Read() - amountToTransfer);
yourAccount.Write(yourAccount.Read() + amountToTransfer);
throw new ApplicationException("Money transaction failed!");
~PostSharp~Laos~Implementation.~aspect~1.OnSuccess(args);
}
}
catch (Exception exception)
{
args.Exception = exception;
~PostSharp~Laos~Implementation.~aspect~1.OnException(args);
switch (args.FlowBehavior)
{
case FlowBehavior.Continue:
case FlowBehavior.Return:
return;
}
throw;
}
finally
{
~PostSharp~Laos~Implementation.~aspect~1.OnExit(args);
}
}
I greyed out the original method body so you can more clearly what PostSharp has woven around it. Basically it´s a try-catch-block which calls the On...() methods of NstmAtomicAttribute. Where .NET needs context bound objects and proxies at runtime to let you intercept calls PostSharp does it by statically inserting code which calls your interception methods.
Aspect oriented transactional data
Since success was so easy to gain with PostSharp I did not want to stop with transactional methods. Wouldn´t it be nice to hide transactionality almost altogether? If enhancer based O/R Mappers can hide persistence behind an attribute and don´t require object creation through factories, why shouldn´t I be able to do the same? If SXM falls short of this, that´s not my problem ;-)
So I set down with PostSharp and had a close look at the different kinds of aspect it provides out of the box. And really there was a way to accomplish what O/R Mapper enhancers have done before. I just had to combine a "generic field" aspect with a "type composition" aspect using a "compound" aspect. And here´s the result:
1 [NstmTransactional]
2 public class Account
3 {
4 private int amount;
5 private int maxOverdraftAmount;
6
7 public Account()
8 {
9 this.amount = 0;
10 this.maxOverdraftAmount = 0;
11 }
12
13 public Account(int initialAmount, int maxOverdraftAmount)
14 {
15 this.amount = initialAmount;
16 this.maxOverdraftAmount = maxOverdraftAmount;
17 }
18
19 public int Amount
20 {
21 get
22 {
23 return this.amount;
24 }
25 set
26 {
27 if (value < maxOverdraftAmount)
28 throw new ApplicationException(...);
29 this.amount = value;
30 }
31 }
32 }
Put the [NstmTransactional] on an ordinary class to make it transactional. That´s it. It´s the same as wrapping INstmObject<T> around it. NSTM will care for buffering writes to instances and committing any changes at the end of a transaction. The money transfer example thus looses almost any trait of its transactionality:
35 static Account myAccount, yourAccount;
36
37 static void Main()
38 {
39 myAccount = new Account(1000, 0);
40 yourAccount = new Account(500, 0);
41
42 try
43 {
44 TransferMoney(1001);
45 }
46 catch (Exception ex)
47 {
48 Console.WriteLine("*** {0}", ex.Message);
49 }
50
51 Console.WriteLine("my account: {0}", myAccount.Amount);
52 Console.WriteLine("your account: {0}", yourAccount.Amount);
53 }
54
55
56 [NstmAtomic()]
57 static void TransferMoney(int amountToTransfer)
58 {
59 myAccount.Amount -= amountToTransfer;
60 yourAccount.Amount += amountToTransfer;
61 }
This is just ordinary object oriented code. (Please don´t get too critical with my account business logic ;-) With PostSharp´s AOP the impact of Software Transactional Memory on your code is reduced to two attributes: make classes transactional with [NstmTransactional] and wrap transactions around methods with [NstmAtomic].
If you don´t have a class you want to make transactional, but just a scalar type or a struct, then use NstmTransactional<T>:
1 static NstmTransactional<int> myAccount, yourAccount;
2
3 static void Main()
4 {
5 myAccount = 1000;
6 yourAccount = 500;
7
8 try
9 {
10 TransferMoney(1001);
11 }
12 catch (Exception ex)
13 {
14 Console.WriteLine("*** {0}", ex.Message);
15 }
16
17 Console.WriteLine("my account: {0}", myAccount.Value);
18 Console.WriteLine("your account: {0}", yourAccount.Value);
19 }
20
21
22 [NstmAtomic()]
23 static void TransferMoney(int amountToTransfer)
24 {
25 myAccount.Value -= amountToTransfer;
26 yourAccount.Value += amountToTransfer;
27
28 if (myAccount < 0)
29 throw new ApplicationException("No overdraft allowed!");
30 }
NstmTransactional<T> works almost like Nullable<T>. It´s just for scalar and value types. However NstmTransactional<T> is a class, not a struct! This is due to the need for identity and addressability of transactional data. When a transaction commits it needs to update a memory location it knows the address of. That´s not possible for value types which live on the stack. So if you want a value type to be transactional you need to be make it into an object.
Please note: [NstmAtomic] keeps only track of changes to one level of an object hierarchy! If you want to make more than one level transactional, you need to put [NstmAtomic] on all classes on all levels!
In this example a contact has an address. If you want to work transactionally with both, it´s not sufficient to just adorn the Contact class with [NstmAtomic], though! The attribute does not cause NSTM to track changes recursively on all objects pointed to by a Contact. Rather you need to make Address also transactional.
1 [NstmTransactional]
2 class Contact
3 {
4 private string name;
5
6 public string Name
7 {
8 get { return name; }
9 set { name = value; }
10 }
11
12 private Address location;
13
14 public Address Location
15 {
16 get { return location; }
17 set { location = value; }
18 }
19 }
20
21 [NstmTransactional]
22 class Address
23 {
24 private string city;
25
26 public string City
27 {
28 get { return city; }
29 set { city = value; }
30 }
31 }
With all levels of the object model made transactional code like this works correctly:
1 Contact c = new Contact();
2 c.Name = "John Doe";
3 c.Location = new Address();
4 c.Location.City = "Hamburg";
5
6 using (INstmTransaction tx = NstmMemory.BeginTransaction())
7 {
8 c.Name = "Peter Doe";
9 c.Location.City = "Berlin";
10 }
11
12 Console.WriteLine(c.Name);
13 Console.WriteLine(c.Location.City);
Neither the contact name nor the location´s city are changed, since the transaction is aborted.
This of course means, you cannot use the .NET collection classes as is. For example you could set up a contact with several addresses like this:
1 [NstmTransactional]
2 class ContactWithManyAddresses
3 {
4 private string name;
5 private List<Address> addresses;
6
7 public ContactWithManyAddresses()
8 {
9 this.addresses = new List<Address>();
10 }
11
12 public string Name
13 {
14 get { return name; }
15 set { name = value; }
16 }
17
18 public List<Address> Addresses
19 {
20 get { return this.addresses; }
21 }
22 }
Then you would want to be able to do the following:
100 ContactWithManyAddresses c = new ContactWithManyAddresses();
101 c.Name = "John Doe";
102 Address a = new Address();
103 a.City = "Hamburg";
104 c.Addresses.Add(a);
105
106 using (INstmTransaction tx = NstmMemory.BeginTransaction())
107 {
108 c.Name = "Peter Doe";
109 c.Addresses[0].City = "Berlin";
110
111 a = new Address();
112 a.City = "Munich";
113 c.Addresses.Add(a);
114 }
115
116 Console.WriteLine(c.Name);
117 foreach (Address b in c.Addresses)
118 Console.WriteLine(b.City);
But the output at the end of this code would surprise you:
The name of the contact and the city of the first address were rolled back correctly. But there is a second city in the list of addresses without a name. That´s the object to which the code assigned "Munich" and which was added to the address collection. The city of this address also got rolled back correctly, but the entry in the ArrayList was not. That´s because standard .NET collections are not transactional.
So, how then should you model 1:n relationships between transactional objects? True transactional collections are needed. I´m already working on some, so stay tuned. Or start building your own transactional list, queue, stack, tree etc. and let me know.
Preliminary conslusion
I´ve presented you my view of Software Transactional Memory for .NET. NSTM is written in C#, so it´s fully managed code. You can download the sources and play around with it. What will it gain you in terms of ease, productivity, or performance? Find out yourself. It´s programming model at least promises to make many multithreading tasks much easier. No need to think about locking, but rather work on on graphs of objects like on databases: open a transaction, do your work, commit the transaction. If something goes wrong, no changes are visible. That´s also an interesting propsal for GUI programming where you often want to display objects in dialogs, let the user enter changes, but also let him discard all changes. Especially with nested dialogs the nested NSTM transactions could help quite a bit to keep the frontend code straightforward, where IEditableObject falls short.
Enjoy!
PS: NSTM is not finished, of course. I´ll continue working on transactional collections. And there are some concepts from STM research I´d like to add, like a Retry() operation and blocking reads and notifications.
Download
You can download NSTM (short for .NET Software Transactional Memory) from the Google project hosting site.
Enjoy - and let me know how you like it, how it performs for you, or what you think needs to be improved.
Installation
- Download PostSharp from www.postsharp.org and install it.
- Download NSTM and install it by unzipping the file.
- Open the solution in the NSTM directory and try to compile it.
(It might fail due to incorrect references to PostSharp assemblies. The remedy is to update the PostSharp.Laos and PostSharp.Public references in the NSTM projects.)
There are a lot of NUnit compatible tests in the source code. You can use Testrunner like I do to run them in VS2005, or you can use NUnit itself. If you want to throw out all tests, just compile in Release mode.
Resources
[1] Juval Löwy, WCF Transaction Propagation, http://msdn.microsoft.com/msdnmag/issues/07/05/Foundations/default.aspx?loc=en
So far I´ve described my own .NET Software Transactional Memory´s (NSTM) API for managing transactions. It´s close to what you are used to from relational databases, I´d say. But still, it´s my own API and it stands beside what .NET already provides in terms of transactions. With System.Transactions there is already a general way to work transactionally across different resources like database and message queue, so it would be nice if NSTM was another such resource.
Juval Löwy [1] has described how this can be accomplished for in-memory data structures. However, making a data structure transactional like he did with the .NET collections does not make it threadsafe. .NET transactions - although attached to a thread - are not designed help make multithreading easier. In addition they cannot be nested truely. Also the data duplication in Juval´s collections is very coarse grained so it will become pretty slow pretty quickly once they are growing larger. Nevertheless the article is worth reading and provides very helpful insights in how .NET transactions work. It helped me a great deal making NSTM compatible with System.Transactions.
From the point of view of System.Transactions a NSTM transaction is a resource. Its state - the transaction log (txlog) - is changed during a .NET transaction and either discarded at the end if the .NET transaction is rolled back, or committed which means the transaction log is applied to the transactional objects (txo).
Recognition of .NET transactions is not switched on automatically, though. To check, if a .NET transaction is running and whether a NSTM transaction is already enlisted with it, is somewhat costly. NSTM needs to walk up the stack of active transactions for this. But you can switch on System.Transaction integration easily with the flag NstmMemory.SystemTransactionMode:
- EnlistOnAccess: NSTM checks on each access to a txo whether a .NET transaction is running. If there is no NSTM enlisted with the .NET transaction it begins a new NSTM transaction, enlists it, and will commit it/roll it back, when the .NET transaction ends.
- EnlistOnBeginTransaction: NSTM checks only for a .NET transaction when a NSTM transaction is explicity started using NstmMemory.BeginTransaction(). If a .NET transaction is running, the new NSTM transaction is enlisted with it. (To be more precise, NSTM even creates two nested transactions: the outer transaction is enlisted with the .NET transaction and the inner transaction is passed back to the application. That way the application can commit/rollback the inner transaction as usual and still get the final vote on the changes automatically from the out transaction coupled to the .NET transaction.)
- Ignore: NSTM does not care if a .NET transaction is already running. All NSTM transactions are independent of System.Transactions.
Here some sample code showing how to use NSTM with System.Transactions:
1 NstmMemory.SystemTransactionMode = NstmSystemTransactionMode.EnlistOnAccess;
2
3 INstmObject<int> o = NstmMemory.CreateObject<int>(1);
4 using (TransactionScope tx = new TransactionScope())
5 {
6 o.Write(2);
7 tx.Complete();
8 }
9 Console.WriteLine(o.Read());
10
11 NstmMemory.SystemTransactionMode = NstmSystemTransactionMode.EnlistOnBeginTransaction;
12 using (TransactionScope tx = new TransactionScope())
13 {
14 using (INstmTransaction txNSTM = NstmMemory.BeginTransaction())
15 {
16 o.Write(3);
17 txNSTM.Commit();
18 }
19 tx.Complete();
20 }
21 Console.WriteLine(o.Read());
In line 1 .NET transaction recogniction is switched on. From then on each read/write access to a txo like o checks, if a .NET transaction is running and if so creates an implicit new NSTM transaction. That way the change to o get committed when the .NET transaction is finished in line 7. Please note, this option - although most convenient - is quite expensive since it requires transactional objects to check upon each and every call whether to create a NSTM transaction or not.
This is why the second example is more economic: It just checks for a .NET transaction in line 14. The price to pay for this option is an explicit NSTM transaction, i.e. a little less convenient programming model.
What´s next?
In my previous posting I said, System.Transactions integration was the final piece missing for your understanding of NSTM. But that´s not true anymore. Since then I stumbled across a very cool tool and have spiced up NSTM a bit. Stay tuned for automatic transactions like with COM+ and truely transparent transactionality for your classes.
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
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:
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.
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
Now that the basic data unit of my .NET Software Transactional Memory (NSTM) has been introduced - transacational objects (txo) aka INstmObject - who implement the Isolation property of transactions, the question is, where Atomicity comes from. Enter: the transaction log.
Recording Memory Interactions
The transaction log (txlog) records all objects you tackle during during a transaction. Whenever you write to a txo that´s logged in the txlog. Whenever you read from a txo that´s logged in the txlog. So the txlog contains a list of all objects interacted with in a transaction including their current values. Even though my previous posting might have suggested txo maintain a clone for their value it is in fact the txlog attached to each transaction.
When you read from or write to a txo you don´t really directly access its value. Rather the INstmObject goes to the current transaction and asks it what to do, which value to return or where to store a new value. The transaction then consults with its transaction log:
- If an object is accessed for the first time during the transaction a log entry for it is added to the txlog.
- If a txo is written to, the new value is put into its txlog entry instead of the txo itself. This is to isolate changes made to the same txo in different transactions from each other.
- If a txo is read from, the transaction checks which value to return. If a new value is already present then that´s chosen. If no new value has been assigned the current value is returned. Either the real current value from the txo - or the current value as cloned on the first read access if the clone option is CloneOnRead.
- Also the txo is validated if the transaction´s isolation level is Serializable. That means the current version number of the txo is compared to the version number when it was first access during the transaction. Validation fails if those versions do not match, which means some other transaction has committed changes to the object in the meantime. This is to avoid inconsistencies in the form of different values read from the same txo during a transaction. If you want to allow such changes then set the isolation level to ReadCommitted.
Any changes to transactional objects during a transaction are accumulated in the transaction log. Txo are thus never changed directly by an application. This provides Isolation and the first half of Atomicity: nothings happens to transactional objects if a transaction fails. Because if it fails, all changes recorded in the txlog are lost.
Ending a Transaction
A transaction can be ended in two ways: either by rolling it back and discarding all changes or by committing it.
Rolling back is easy: the transaction log simply is discarded. That´s it. No further effort is needed. No locks were helt on txo which would need unlocking. No changes were made which would need to be undone.
Committing a transaction on the other hand is a two step process:
- First all txo read from with more than just PassingReadOnly mode are validated. (Currently this is also true for ReadWrite mode txo, but I´m unsure if that´s necessary. Also currently I´m not content with how to switch between validation on Commit() only and validation on each Read().) During validation all transactional objects opened in ReadWrite mode also are locked. This is to freeze the current view on transactional memory for the duration of the commit. No other transactions must commit at the same time to the same txo.
Where locking comes into play deadlocks need to be avoided. Therefore all txos are kept in a sorted list so each transaction would lock them in the same order. This is a common way to give deadlocks no chance.
If any transactional object cannot be validated the commit is aborted and the transaction is rolled back. - Second all locked txos written to are visited again to copy their new values to the txo itself. At the same time the version number of each txo is incremented to allow for easy optimistic locking aka validation. Afterwards the txo is unlocked.
By locking modified txos (for a very short time) during commit Atomicity is ensured. An application either sees no changes at all when a transaction is rolled back - or all changes at once after Commit() has finished and all modified objects have been updated and unlocked.
What´s next?
Now that I´ve explained how a single transaction works it´s time to look at how mutiple concurrent transactions on one or more threads are managed. Stay tuned if you are interested to see how NSTM implements truely nested transactions.