Multithreaded Apps Made Easy Using Aspects - Part 2

by Gael Fraiteur on 11 Nov 2010

In the first part of this article, I’ve shown how to use aspects to force method run on a specific type of thread – background or GUI. But putting operations on threads is only one side of the equation, and arguably the easiest one. What is much more difficult, is to avoid conflicts when accessing shared resources from different threads.

To get the source code of this article, install PostSharp, and go to directory:
C:\Program Files\PostSharp 2.0\Samples\.NET Framework 3.5\CSharp\Threading.

Because of this, we need to address the following issues:

  • How do I ensure that an object is in a consistent state when a thread reads it? How can I be sure that another thread is not modifying it at that particular moment?
  • How do I avoid two threads concurrently modifying the same object and breaking its consistency?
  • How do I prevent deadlocks?

In object-oriented programming, it often occurs that a significant part of the object model is a shared resource. This is typically the case with model objects in a Model-View-Controller. If the controller is allowed to modify the model from different threads, proper thread synchronization is necessary.

The Design Pattern

The first and most important thing to do when coping with thread synchronization is to identify good design patterns – there is no alternative to good design.

The design pattern I chose here is based on reader-writer locks (see the class ReaderWriterLockSlim). These locks allow concurrent reader threads, but the writer must have exclusive access. That is, a writer must wait for other readers, or other writers, to finish before starting, and will prevent them from starting until the writer finishes. Using these locks results in minimal thread contention (i.e. threads wait only minimally for each other) and deadlocks. Alas, they also result in an extensive amount of plumbing code.

Our design pattern possibly associates a lock with each object instance. However, if instance-level consistency is not enough, many instances can share the same lock. This is typically the case when one object is aggregated into another one. For instance, one may want an order and its order lines to always be consistent. In that case, all instances forming together in the same order would share the same lock.

As part of the design pattern, we decided that all synchronized objects should implement the IReaderWriterSynchronized interface:

public interface IReaderWriterSynchronized
{
    ReaderWriterLockSlim Lock { get; }
}

This interface will be useful when implementing the aspects.

Additionally, since implementing IReaderWriterSynchronized is still writing plumbing code, we would prefer a custom attribute to do it for us. Let’s call it ReaderWriterSynchronizedAttribute.

We further define custom attributes that, when applied to methods or property accessors, determine which kind of access to the object is required: ReaderAttribute, WriterAttribute or ObserverAttribute.

Any method that modifies the object, should be annotated with the [Writer] custom attribute. Methods that read more than one field of the object should also be annotated with the [Reader] custom attribute (it is useless to synchronize methods or property getters performing a single read access, because the operation is always consistent).

Let’s set aside the observer lock for the moment. The next piece of code illustrates a synchronized class: all its public members are guaranteed to perform consistently in a multithreaded environment.

[ReaderWriterSynchronized]
public class Person 
{
  public string FirstName { get; [Writer] set; }

  public string LastName { get; [Writer] set; }

  public string FullName
  {
      [Reader]
      get { return this.FirstName + " " + this.LastName; }
  }
}

Observer Locks

In an MVC design, the view is bound to model objects. Model objects expose events that are raised when they are updated (typically the PropertyChanged event of the INotifyPropertyChanged interface). The view (for instance, a data-bound WPF control) subscribes to this event.

In certain cases, it is crucial that the object does not get modified between the time the event is fired and the time it is processed by the view. One example is with observable collections in WPF (INotifyCollectionChanged interface). Since the NotifyCollectionChangedEventArgs object contains item indices, it is essential that these indices still refer to the same items when the event is processed.

So at first sight, it seems that we need to invoke events inside the writer lock, doesn’t it? Wrong; this would cause a deadlock. Indeed, remember that the view is bound to the GUI thread. Therefore, the PropertyChanged event handler is dispatched to the GUI thread:

[GuiThread]
private void person_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  this.label.Text = ((Person)e).FullName;
}

When you bind a control to a domain object using WPF Data Binding, the thread dispatching is completed transparently.

When evaluating the FullName property, the GUI thread will require a read lock on the model object. However, the object is already locked by the writer! The GUI thread would therefore be required to wait for the worker thread to release the writer lock, but the worker thread has to wait until the GUI thread finishes the processing of the PropertyChanged event: we clearly would have a deadlock.

Therefore, we need a locking level that would prevent any other writer, but would allow concurrent readers.

This needed lock is called an upgradable reader lock: a reader lock that can be upgraded to a writer lock, then downgraded back to a reader lock. Upgradable readers allow concurrent readers, but forbid concurrent writers or upgradable readers. This is exactly what we need.

Instead of acquiring a writer lock, WriterAttribute will always acquire an upgradeable read lock and upgrade it to a write lock. As a result, it will be possible to downgrade it into an ObserverAttribute,

The following listing demonstrates how the Person class can be made observable, while ensuring multi-thread safety and avoiding deadlocks:

[ReaderWriterSynchronized]
public class Person : INotifyPropertyChanged
{
  private string firstName;
  private string lastName;

  public string FirstName
  {
      get { return this.firstName; }

      [Writer]
      set
      {
          this.firstName = value;
          this.OnPropertyChanged("FirstName");
      }

  }

  public string LastName
  {
      get { return this.lastName; } 

      [Writer]
      set
      {
          this.lastName = value;
          this.OnPropertyChanged("LastName");
      }
  }

  public string FullName
  {
      [Reader]
      get { return this.firstName + " " + this.lastName; }
  }


  [Observer]
  protected virtual void OnPropertyChanged(string propertyName)
  {
      if (this.PropertyChanged != null)
          this.PropertyChanged(this, 
      new PropertyChangedEventArgs(propertyName));
  }

  public event PropertyChangedEventHandler PropertyChanged;
}
You can also implement the interface INotifyPropertyChanged using PostSharp. This article is just about multithreading, so we won’t cover it.

Check List

We are now complete with our design pattern and need to distribute the following instructions to our team:

  • During analysis, identify which groups of objects should share locks. Most of the time, objects sharing the same lock form a tree.
  • Annotate synchronized objects with the [ReaderWriterSynchronized] custom attribute, or implement the IReaderWriterSynchronized interface manually.
  • Annotate with the [Reader] custom attribute all public read-only methods that perform more than one read operation.
  • Annotate with the [Writer] custom attribute all public methods modifying the object.
  • Implement events according to the standard design guidelines, but annotate the event-handling method (for instance OnPropertyChanged) with the [Observer] custom attribute.

Now, let’s look at the implementation of these custom attributes.

Implementing the ReaderWriterSynchronizedAttribute

This aspect must introduce a new interface into the target type and implement this interface. This can be realized easily by deriving the IntroduceInterface advice (an advice, in AOP jargon, is any code transformation).

[Serializable]
[IntroduceInterface(typeof(IReaderWriterSynchronized))]
public class ReaderWriterSynchronizedAttribute : InstanceLevelAspect, IReaderWriterSynchronized
{
  public override void RuntimeInitializeInstance(AspectArgs args)
  {
    base.RuntimeInitializeInstance( args );
    this.Lock = new ReaderWriterLockSlim();
  }

  [IntroduceMember]
  public ReaderWriterLockSlim Lock { get; private set; }
}

Because we derive our class from InstanceLevelAspect, our aspect will have the same lifetime as the instances of the classes to which it applies. In other words, we’ll have one instance of our aspect per instance of the target class. So fields of our aspects are actually “equivalent” to fields of the target class. The RuntimeInitializeInstance method is invoked from the constructor of the target classes; this is where we have to create the instance of the lock object. The IntroduceMember custom attribute instructs PostSharp to add the Lock property to the target class. The IntroduceInterface custom attribute does the same with the interface.

Implementing ReaderAttribute, WriterAttribute and ObserverAttribute

Before implementing an aspect, it’s good to ask oneself: how would we do it without aspects? What would the expanded code look like? To answer these questions, we would first have to determine if we already hold the lock and, if not, acquire it. We would have to enclose the whole method body in a try block and release the lock, if it was acquired, in the finally block. So our methods would look like this:

void MyMethod()
{
    bool acquire = !(this.myLock.IsWriteLockHeld || 
                     this.myLock.IsReadLockHeld ||
                     this.myLock.IsWriteLockHeld);

    if ( acquire )
        this.myLock.EnterReadLock();

    try
    {
        // Original method body.
    }
    finally
    {
        if ( acquire )
            this.myLock.ExitReadLock();
    }
}

PostSharp provides an appropriate kind of aspect for this transformation: OnMethodBoundaryAspect. It wraps the original method body inside a try…catch…finally block and gives us the opportunity to execute code before the method, upon successful execution, upon exception, and in the finally block. This is exactly what we need.

The following is the code for the Reader attribute. Note that the Writer and Observer attributes are similar.

[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method, 
                        TargetMemberAttributes = MulticastAttributes.Instance)]
public class ReadLockAttribute : OnMethodBoundaryAspect
{
  public override void OnEntry(MethodExecutionArgs args)
  {
    ReaderWriterLockSlim @lock = ((IReaderWriterSynchronized) args.Instance).Lock;

    if (!@lock.IsReadLockHeld && 
        !@lock.IsUpgradeableReadLockHeld &&
        !@lock.IsWriteLockHeld)
    {
        args.MethodExecutionTag = true;
        @lock.EnterReadLock();
    }
    else
    {
        args.MethodExecutionTag = false;
    }
  }

  public override void OnExit(MethodExecutionArgs args)
  {
    if ((bool) args.MethodExecutionTag)
    {
      ((IReaderWriterSynchronized) args.Instance).Lock.ExitReadLock();
    }
  }
}

In the preceding code, we implemented two handlers: OnEntry and OnExit. In order to obtain access to the ReaderWriterLockSlim object, we need to cast the target instance (available on the args.Instance property) to the IReaderWriterSynchronized interface and retrieve the Lock property.

The OnEntry method needs to store the information somewhere, whether the lock was acquired by us or not. Indeed, this information will be required by the OnExit method. For this purpose, we can use the args.MethodExecutionTag property. Whatever a handler stores in this property will be available to the other handlers.

Note the presence of the MulticastAttributeUsage custom attribute on the top of our class. It means that the aspect is to be used on instance methods only, so it is not to be used on constructors or on static methods.

Conclusions

Multithreaded programming can be simplified by adequately raising the level of abstraction. But why should every programmer care about synchronization primitives? In an ideal world, it should be enough if he or she annotates methods with a custom attribute determining the thread affinity or the locking level required by the method.

This is what we have demonstrated in this article by using six aspects: OnWorkerThread and OnGuiThread for thread affinity, and ReaderWriterSynchronized, Reader, Writer and Observer for the locking level.

However, multithreading is just one possible field of application of aspect-oriented programming. Caching, transaction management, exception handling, performance monitoring, and data validation are other concerns where aspect-oriented programming can advantageously be applied.

By providing a new way to encapsulate complexity, aspect-oriented programming results in shorter, simpler and more readable code, therefore being less expensive to write and maintain.