Software Transactional Memory VI - Becoming Aspect Oriented with PostSharp
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