views:

70

answers:

1

I have a .NET 4 WCF service that maintains a thread-safe, in-memory, dictionary cache of objects (SynchronizedObject). I want to provide safe, concurrent access to read and modify both the collection and the objects in the collection. Safely modifying the objects and the cache can be accomplished with reader-writer locks.

I am running into trouble providing read access to an object in the cache. My Read method returns a SynchronizedObject, but I do not know how to elegantly ensure no other threads are modifying the object while WCF is serializing the SynchronizedObject.

I have tried placing the Read return clause inside the read-lock and setting a breakpoint in a custom XmlObjectSerializer. When the XmlObjectSerializer::WriteObject(Stream,object) method is called, a read-lock is not held on the SynchronizedObject.

I am specifically concerned with the following scenario:

  1. Thread A calls Read(int). Execution continues until just after the return statement. By this point, the finally has also been executed, and the read lock on the SynchronizedObject has been released. Thread A's execution is interrupted.
  2. Thread B calls Modify(int) for the same id. The write lock is available and obtained. Sometime between obtaining the write lock and releasing it, Thread B is interrupted.
  3. Thread A restarts and serialization continues. Thread B has a write-lock on the same SynchronizedObject, and is in the middle of some critical section, but Thread A is reading the state of the SynchronizedObject and thus returns a potentially invalid object to the caller of Read(int).

I see two options:

  • Maintain a custom XmlObjectSerializer that grabs the read-lock before calling the base.WriteObject(Stream, object) method, and releases it after. I do not like this option because sub-classing and overriding a framework serialization function to perform a certain action if a the object to be serialized matches a certain type smells to me.
  • Create a deep-copy of a SynchronizedObject in the Read method while the read-lock is held, release the lock, and return the deep copy. I do not like this option because there will be many sub-classes of SynchronizedObject that I would have to implement and maintain correct deep-copiers for and deep-copies could be expensive.

What other options do I have? How should I implement the thread-safe Read method?

I have provided a dummy Service below for more explicit references:


    public class Service : IService
    {
        IDictionary<int, SynchronizedObject> collection = new Dictionary<int, SynchronizedObject>();
        ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();

        public SynchronizedObject Read(int id)
        {
            rwLock.EnterReadLock();
            try
            {
                SynchronizedObject result = collection[id];
                result.rwLock.EnterReadLock();
                try
                {
                    return result;
                }
                finally
                {
                    result.rwLock.ExitReadLock();
                }
            }
            finally
            {
                rwLock.ExitReadLock();
            }
        }

        public void ModifyObject(int id)
        {
            rwLock.EnterReadLock();
            try
            {
                SynchronizedObject obj = collection[id];
                obj.rwLock.EnterWriteLock();
                try
                {
                    // modify obj
                }
                finally
                {
                    obj.rwLock.ExitWriteLock();
                }
            }
            finally
            {
                rwLock.ExitReadLock();
            }
        }

        public void ModifyCollection(int id)
        {
            rwLock.EnterWriteLock();
            try
            {
                // modify collection
            }
            finally
            {
                rwLock.ExitWriteLock();
            }
        }
    }

    public class SynchronizedObject
    {
        public ReaderWriterLockSlim rwLock { get; private set; }

        public SynchronizedObject()
        {
            rwLock = new ReaderWriterLockSlim();
        }
    }
A: 

New answer

Based on your new information and clearer scenario, I believe you want to use something similar to functional programming's immutability feature. Instead of serializing the object that could be changed, make a copy that no other thread could possibly access, then serialize that.

Previous (not valuable) answer

From http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.enterwritelock.aspx:

If other threads have entered the lock in read mode, a thread that calls the EnterWriteLock method blocks until those threads have exited read mode. When there are threads waiting to enter write mode, additional threads that try to enter read mode or upgradeable mode block until all the threads waiting to enter write mode have either timed out or entered write mode and then exited from it.

So, all you need to do is call EnterWriteLock and ExitWriteLock inside ModifyObject(). Your attempt to make sure you have both a read and a write lock is actually stopping the code from working.

John Fisher
I am not trying to enter read and write locks on the same object. In ModifyObject, I am reading from the collection (read lock on collection) and writing to the object (write lock on object). The code is not deadlocking. I have added a specific scenario I am concerned about to the question.
burkemw3
Sorry, I missed that. Thanks for clearing up the question.
John Fisher
That's alright, threading is interesting, but sure can be hard. Your revised answer is similar to the deep-copy option I outlined above. Do you have any elaborations on why you think maintaining and using deep-copy algorithms is a better solution than obtaining the lock in a custom serializer?
burkemw3
I fought the serializer before. The situation I ran into was one where the serializer created objects, but the constructor never executed. It apparently has back doors to increase performance, but can really hamper development in some edge cases. The copy method would prevent the serializer from using a back door that would cause issues. (You could also write your own serializer.)
John Fisher