[UPDATE: Please see the first comment below about some potential problems with the ReaderWriterLock
class, and some solutions. -sj]
Recently I was encountering an issue on a SharePoint site where some site properties were getting cached in the HttpContext.Current.Cache
. The logic went roughly like this:
- Read values from a SharePoint list
- Put the values into the cache
I'm oversimplifying, of course, but those are the important steps as they relate to this blog post. I had implemented proper locking, or so I thought, by using the C#
lock
statement. The pattern I learned long ago when dealing with creating a shared cache that multiple threads can access is to do something like this:
- Try accessing the object.
- If you get it, great! Go forth and use it.
- If you don't get it, try obtaining a lock (i.e. you're about to enter a critical section).
- Once you obtain the lock, try accessing the object again, just in case another thread beat you to it.
- If you get it, great! Release the lock, go forth and use it.
- If you still don't get it, create it, then put it in the cache.
- Exit the critical section.
This pattern prevents unnecessary locking if the object already exists in the cache. It also prevents the object from being created a second time.
As it turns out, the HttpContext.Current.Cache
object isn't entirely thread-safe. Even though I was locking and such, I was unable to retrieve the site properties I was creating. To the second thread, it appeared that the properties were not in the cache, and the logic would get invoked to create it again.
That's when I did some research and discovered the System.Threading.ReaderWriterLock
class. This gem allows multiple threads to access an object, but only allows one thread at a time to write to that object. The object has both a reader lock and a writer lock. Many reader locks are granted as long as there are no writer locks granted (or pending). If there is a writer lock that is being requested, it blocks until all the reader locks are released.
Another cool feature is that, once you have a reader lock, you can call "UpgradeToWriterLock
" if you discover you need to write to the object. Once you're done, you then "DowngradeFromWriterLock
" and then "ReleaseReaderLock
". Or you can simply call "ReleaseLock
", I believe, to release all locks obtained.
Best practice for using this object is to wrap it in a try…finally
block of code. In the try
part, acquire the lock and then do your stuff. In the finally
part, release the lock. In this manner you ensure that you always release the locks you acquire, and the system never gets "out of balance".
Here's how I used it to solve my problem:
- Get a reader lock (will block only if another thread is writing)
- Try to get the properties
- If obtained, exit and use the properties (the finally kicks in to release the lock)
- If not obtained, upgrade to a writer lock (will block if any other reader or writer locks are being held).
- Try to get the properties again (nested
try
block) - If obtained, exit and use the properties (the
finally
kicks in to downgrade the lock, and then the outer finally kicks in to release the reader lock) - If not obtained, create it and return it (the
finally
blocks kick in as described in step 6)
It's more complex than using a simple lock
statement, but I found that it was necessary in this case.
One final implementation note: the ReaderWriterLock
object was stored in a "private static readonly
" member variable so that it was accessible from any method that needed it, and so that all threads were accessing the same object.