Open sandboxFocusImprove this doc

Reader/Writer Synchronized Threading Model

When a class instance is concurrently used by multiple threads, accesses must be synchronized to prevent data races, which typically result in data inconsistencies and corruption of data structures. The Reader/Writer Synchronized Threading Model uses locks to allow several read-only methods to execute simultaneously on one instance, but guarantee that writer methods have exclusive access.

The Reader/Writer Synchronized Threading Model is implemented by the ReaderWriterSynchronizedAttribute aspect. It requires you to annotate all public methods of your synchronized classes with the ReaderAttribute and WriterAttribute custom attributes.

Why use the reader-writer synchronized pattern?

Problems without locks

Consider the following example of an Order class which stores an amount and a discount:

class Order  
{        
    int Amount { get; private set; }
    int Discount { get; private set; }

    public int AmountAfterDiscount
    {
        get { return this.Amount - this.Discount; }
    }
     
    public void Set(int amount, int discount)
    {
        if (amount < discount)
            throw new InvalidOperationException();

        this.Amount = amount;
        this.Discount = discount;
    }
}

In this example, the Set method writes to the Amount and Discount members, while the AmountAfterDiscount property reads these members. In a single-threaded program, the AmountAfterDiscount property is guaranteed to be positive or zero. However, in a multithreaded program, the AmountAfterDiscount property could be evaluated in the middle of the Set operation, and return an inconsistent result.

Problems of the lock keyword

The easiest way to synchronize accesses to a class in C# is to use the lock keyword. However, this practice cannot be generalized for two reasons:

  • The use of exclusive locks often results in high contention and therefore low performance because many threads queue to access the same resource;

  • Applications relying on exclusive locks are prone to deadlocks because of cyclic waiting dependencies.

Problems of reader-writer locks

Reader-writer locks take advantage of the fact that most applications involve much fewer writes than reads, and that concurrent reads are always safe. Reader-writer locks ensure that no other thread is accessing the object when it is being written. Reader-writer locks are normally implemented by the .NET classes ReaderWriterLock or ReaderWriterLockSlim. The following example shows how ReaderWriterLockSlim would be used to control reads and writes in the Order class:

class Order
{
    private ReaderWriterLockSlim orderLock = new ReaderWriterLockSlim();

    public decimal Amount { get; private set; }
    public decimal Discount { get; private set; }

    public decimal AmountAfterDiscount
    {
        get 
        {
            orderLock.EnterReadLock();
            decimal result = this.Amount - this.Discount;
            orderLock.ExitReadLock();
            return result;
        }
    }

    public void Set(decimal amount, decimal discount)
    {

        if (amount < discount)
        {
            throw new InvalidOperationException();
        }

        orderLock.EnterWriteLock();
        this.Amount = amount;
        this.Discount = discount;
        orderLock.ExitWriteLock();
    }    
}

However, working directly with the ReaderWriterLock and ReaderWriterLockSlim classes has disadvantages:

  • It is cumbersome because a lot of code is required.

  • It is unreliable because it is too easy to forget to acquire the right type of lock, and these errors are not detectable by the compiler or by unit tests.

So, not only the direct use of locks results in more lines of code, but it won’t reliably prevent nondeterministic data structure corruptions.

Making a class reader-writer synchronized

PostSharp Threading Pattern Library has been designed to eliminate nondeterministic data corruptions while reducing the size of thread synchronization code to the absolute minimum (but not less).

The ReaderWriterSynchronizedAttribute aspect implements the threading model (or threading pattern) based on the reader-writer lock, with the following principles:

  • At any time, the object can be open for reading or closed for reading.

  • Methods define their required access level using ReaderAttribute and WriterAttribute custom attributes (other access levels exist for advanced scenarios)

  • An error will be emitted at build-time or runtime, but deterministically, whenever an object field is being accessed by a method that does not have the required access level on the object.

To apply the ReaderWriterSynchronized threading model to a class:

  1. Add the PostSharp.Patterns.Threading package your project.

  2. Add using PostSharp.Patterns.Threading namespace to your file.

  3. Add the custom attribute [ReaderWriterSynchronizedAttribute] to the class.

  4. Annotate your object model for parent/child relationships as described in Annotating an Object Model for Parent/Child Relationships (Aggregatable).

  5. Add the custom attribute [ReaderAttribute] or [WriterAttribute] to the public and internal methods. Note that it is not necessary to put these attributes on property getters and setters or on events.

The ReaderAttribute attribute causes PostSharp to acquire a lock on the instance whenever the method is invoked. While this lock is held, other threads can also read properties or invoke read-only methods of that instance, but calls to properties or methods marked with WriterAttribute will be blocked until all reads are complete.

Likewise, invoking methods marked with WriterAttribute will lock the instance causing all reads and writes to block until the write has completed and the write lock has been released.

Example

The following code shows the Order class, synchronized with the reader-writer threading pattern:

[ReaderWriterSynchronized]
class Order
{        
    decimal Amount { get; private set; }
    decimal Discount { get; private set; }

    public decimal AmountAfterDiscount
    {
        get { return this.Amount - this.Discount; }
    }
    
    [Writer]
    public void Set(decimal amount, decimal discount)
    {
        if (amount < discount)
            throw new InvalidOperationException();

        this.Amount = amount;
        this.Discount = discount;
    }    
}

Rules enforced by the ReaderWriterSynchronized aspect

The ReaderWriterSynchronizedAttribute aspect emits build-time errors in the following situations:

  • The class contains a public or internal field.

  • The class contains a public method is missing a ReaderAttribute or WriterAttribute custom attribute, or another attribute derived from AccessLevelAttribute. Note that property getters and setters and event accessors do not need to be annotated.

The reader/writer synchronized object will throw a ThreadAccessException whenever some code tries to access a field from a thread that does not own the correct lock, i.e. when a reader method tries to write a field, or when a non-annotated method (e.g. a delegate call) tries to read or write a field.

Raising synchronous events

In some situations, a method with write access needs to allow other threads to read the object before another write is performed on the object. The implementation of INotifyCollectionChanged gives a typical example of this situation. The INotifyCollectionChanged event defined by this interface is typically raised from a write method but is consumed from the user interface thread. The object cannot have changed between the moment the event is raised and it is processed by the UI thread, because the event arguments contain data that relates to the current state of the object. Using only WriterAttribute and ReaderAttribute would either result in deadlocks or in inconsistencies, respectively.

The solution to this problem is to use the YielderAttribute custom attribute, which allows read access from other threads but prevents any other thread from acquiring a writer lock.

Example

In the following example, OrderCollection is a collection of Order objects. In this example, the Add() and Remove() methods are marked with the WriterAttribute attributes. Listeners can be notified about these changes by subscribing to the CollectionChanged event which is exposed through the implementation of INotifyCollectionChanged

Since listeners can be on other threads (e.g. a UI thread), this event is invoked by the Add() and Remove() methods via a method called OnCollectionChanged() which has been marked with the YielderAttribute attribute. This lock ensures that the listener (which may be in another thread space) can read the current state of the collection without the collection being modified by another invocation of the Add() or Remove() operations from another thread.

[ReaderWriterSynchronized]
class OrderCollection : ICollection, INotifyCollectionChanged
{
    ArrayList list = new ArrayList();

    // Details skipped.

    [Reader]
    public int Count
    {
        get
        {
            return list.Count;
        }
    }

    [Writer]
    public void Add(Order o)
    {
        list.Add(o);
        NotifyCollectionChangedEventArgs changedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, o);
        OnCollectionChanged(changedArgs);
    }

    [Writer]
    public void Remove(int index)
    {
        NotifyCollectionChangedEventArgs changedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, list[index]);            
        list.RemoveAt(index);
        OnCollectionChanged(changedArgs);
    }

    [Yielder]
    private void OnCollectionChanged(NotifyCollectionChangedEventArgs changedArgs)
    {
        CollectionChanged(this, changedArgs);
    }

    [Reader]
    public Order Get(int index)
    {
        return (Order)list[index];
    }

    public event NotifyCollectionChangedEventHandler CollectionChanged;
}

Executing long-running write methods

Since write methods require exclusive access to the object, they should complete as quickly as possible. However, this is not always possible. Some long-running write methods really do a lot of write operations (or rely on slow external services) which make them inappropriate for the reader-writer-synchronized model. However, many write methods are actually composed of a lot of read operations but just a few write operations at the end. In this case, it is possible to use a combination of the UpgradeableReaderAttribute and WriterAttribute attributes. The UpgradeableReaderAttribute attribute ensures that no other thread than the current one will be able to acquire a writer lock on the object, so it gives the guarantee that the object is not going to be modified during the method’s execution. A method that holds an upgradeable reader lock can then invoke a method with the WriterAttribute attributes custom attribute. Note that it is important that the writer methods leave the object in a consistent state before exiting, because other threads will be allowed to read the object.

The following example builds on that in the section where the Order class contains a collection of Line objects which make up the order. In the example below, a new method called Recalculate() has been added to Order which iterates through each Line in the collection, tallies up the amount from each, and then stores the total in Amount.

Since the Recalculate method performs a series of reads followed by a write operation (to store the total in Amount), it is marked with the UpgradeableReaderAttribute attribute which ensures that all of the orders that it reads remain locked so that it calculates and writes out the correct total. In addition to this, the set accessor of the Order ’s Amount property has been marked with WriterAttribute:

[ReaderWriterSynchronized]
class Order
{
    // Other details skipped for brevity.

    public decimal Amount
    {
       // The [Reader] attribute optional here is optional because the method is a public getter.
       get;
       
       // The [Writer] attribute is required because, although the method is a setter, this setter is private, 
       // therefore is does not acquire write access by default.
       [Writer] private set;
    }

    [UpgradeableReader]
    public void Recalculate()
    {
        decimal total = 0;
        
        for (int i = 0; i < lines.Count; ++i)
        {
            total += lines[i].Amount;
        }

        this.Amount = total;
    }
}

Working with object trees

Because the Reader/Writer Synchronized model is an implementation of the Aggregatable pattern, all of the same behaviors of the AggregatableAttribute are available. For more information regarding object trees, read Parent/Child, Visitor and Disposable.

Note

Once you have established your parent-child relationships you will need to apply compatible threading models to the child classes. You will want to refer to the Compatibility of Threading Models article to determine which threading model will work for the children of the Read/Writer Synchronized object.

When a ReaderWriterSynchronized object becomes the child of a Synchronized object, it effectively becomes fully Synchronized itself. From that point on, even its Reader methods will require the full lock and will act as though they were Writer methods. When it stops being a child of a Synchronized object, you will again become able to run multiple Reader methods at the same time.

See Also

Other Resources

Compatibility of Threading Models
Reference

ReaderWriterSynchronizedAttribute
WriterAttribute
ReaderAttribute
UpgradeableReaderAttribute
YielderAttribute
ExplicitlySynchronizedAttribute