Skip to content

Thread Synchronization

jrcc edited this page Jan 31, 2013 · 3 revisions

The problem of thread synchronization is often stated as follows:

"If a resource may be accessed by more than one thread, and that resource is not itself designed for concurrent access by multiple threads, then access to that resource must be synchronized."

For example, consider the following very simple Cache class, which uses a .NET Dictionary for internal storage:

public static class Cache
{
   private static Dictionary<string, string> _map = new Dictionary<string, string>();

   public static string Get(string key)
   {
       return _map[key];
   }

   public static void Put(string key, string value)
   {
       _map[key] = value;
   }
}

If we would like to make this cache safe for concurrent use by multiple threads, we need to synchronize access to the underlying Dictionary, because the Dictionary is not itself thread-safe. One way to accomplish this is to apply the C# lock keyword around access to the Dictionary:

public static class Cache
{
   private static Dictionary<string, string> _map = new Dictionary<string, string>();

   public static string Get(string key)
   {
      lock(_map)
      {
         return _map[key];
      }
   }

   public static void Put(string key, string value)
   {
      lock(_map)
      {
         _map[key] = value;
      }
   }
}

Let's call this the "naive" approach. The naive approach works: while one thread is accessing the Dictionary, other threads that need to access the Dictionary are blocked, and therefore the Cache.Get and Cache.Put methods may be safely accessed by many threads. However, the naive approach may have an unnecessarily detrimental effect on performance. Why?

Readers and Writers

The naive approach makes no distinction between "readers" and "writers". The C# lock keyword used in the above code is a mutually exclusive ("mutex") lock, meaning that only one thread may access the cache at a time, regardless of whether it is doing a Get or a Put operation. All threads that wish to access the cache are treated equally. But it turns out that this is overly restrictive. The underlying resource in this case (a .NET Dictionary object) actually is safe for multiple concurrent readers. As long as no one is modifying the Dictionary (adding, over-writing, or removing items), it is perfectly safe for multiple threads to concurrently read from the Dictionary.

Furthermore, a cache typically has many more readers than writers (if this is not the case, then it isn't a very effective cache). Therefore, it would be better if we could allows readers to proceed with read operations without having to wait to acquire a lock, and apply an exclusive lock only when a thread needs to write to the cache.

Read-Write Locks

Fortunately, the .NET framework offers a ready-made solution to this in the form of the [ReaderWriterLockSlim] (http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx) class. This class allows a thread to obtain either a non-exclusive read-lock or an exclusive write-lock, depending on what type of operation it intends to perform on the shared resource (there is also an upgradeable-read-lock, but we'll leave that out of scope here). As long as no thread holds a write-lock, all threads requesting read-lock will be granted concurrent access to the resource. However, when a thread acquires a write-lock, all other threads, including reader threads, must wait the writer thread has releases the write-lock.

In theory, modifying the implementation of the Cache class to use a reader-writer lock will allow the Cache class to achieve much better performance than the simple mutex lock, especially in situations where readers greatly outnumber writers. In practice however, other considerations should be taken into account, such as the overhead of ReaderWriterLockSlim itself (vs. the lock keyword), and whether or not there is enough lock contention to matter (e.g. see this [discussion] (http://stackoverflow.com/questions/4217398/when-is-readerwriterlockslim-better-than-a-simple-lock)).

Regardless of what locking implementation one ultimately chooses for a given situation, thinking about access to shared resources in terms of readers and writers is a useful habit to get into.

Clone this wiki locally