ReaderWriterLockSlim in C# for Read-Heavy Resources
This article provides an overview of the
ReaderWriterLockSlim class in C#, explaining its purpose,
internal locking modes, and practical implementation. You will learn the
specific conditions under which this synchronization primitive
outperforms a standard lock statement, when to avoid it,
and how to properly manage shared state in read-heavy multithreaded
applications.
What is ReaderWriterLockSlim?
ReaderWriterLockSlim is a thread-synchronization
primitive located in the System.Threading namespace. It
coordinates access to a shared resource by distinguishing between read
operations and write operations.
In standard C# thread synchronization, the lock keyword
(which uses Monitor) is exclusive: only one thread can
access the protected resource at a time, regardless of whether it is
reading or modifying data. ReaderWriterLockSlim changes
this dynamic by permitting multiple threads to read concurrently while
enforcing strict, single-thread exclusivity whenever a write occurs.
It was introduced in .NET Framework 3.5 as a lightweight, faster, and
less error-prone replacement for the legacy
ReaderWriterLock class.
The Three Access Modes
ReaderWriterLockSlim supports three distinct locking
states:
- Read Mode (
EnterReadLock/ExitReadLock): Multiple threads can enter read mode simultaneously, provided no thread holds or is waiting for a write lock. - Write Mode (
EnterWriteLock/ExitWriteLock): Grants exclusive access to a single thread. All other reading and writing threads are blocked until the lock is released. - Upgradeable Read Mode (
EnterUpgradeableReadLock/ExitUpgradeableReadLock): A specialized state where only one thread is allowed to read with the option to escalate to a write lock without risking a deadlock. Other normal read threads can still execute concurrently.
When to Use ReaderWriterLockSlim
ReaderWriterLockSlim is not a default replacement for
lock. It introduces higher computational overhead to
acquire and manage than a standard Monitor.
Use it only when your application meets the following criteria:
- High Read-to-Write Ratio: The resource is read significantly more often than it is written (typically 80–90% reads or higher).
- Extended Read Duration: The read operation takes enough time that concurrent readers would otherwise experience noticeable delays waiting in line for an exclusive lock.
- High Contention Among Readers: Many concurrent threads frequently request the resource at the same time.
Common examples include in-memory application caches, configuration stores updated infrequently at runtime, and routing tables.
When to Avoid It
- Write-Heavy Workloads: If writes are frequent, threads will constantly block each other, eliminating the benefits while incurring the lock's overhead.
- Extremely Fast Operations: If a read takes only a
few CPU cycles (such as reading a single variable), the overhead of
managing
ReaderWriterLockSlimwill be slower than a basiclock. - Simple Key-Value Lookups: Built-in thread-safe
collections like
ConcurrentDictionary<TKey, TValue>often provide better performance without manual synchronization.
Implementation Example
Because failing to release a lock can cause deadlocks, always manage
enter and exit calls within try...finally blocks.
using System;
using System.Collections.Generic;
using System.Threading;
public class CacheService : IDisposable
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
public string Get(string key)
{
_lock.EnterReadLock();
try
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
finally
{
_lock.ExitReadLock();
}
}
public void Set(string key, string value)
{
_lock.EnterWriteLock();
try
{
_cache[key] = value;
}
finally
{
_lock.ExitWriteLock();
}
}
public void AddIfNotExists(string key, string value)
{
_lock.EnterUpgradeableReadLock();
try
{
if (!_cache.ContainsKey(key))
{
_lock.EnterWriteLock();
try
{
_cache[key] = value;
}
finally
{
_lock.ExitWriteLock();
}
}
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
public void Dispose()
{
_lock.Dispose();
}
}Summary Checklist
- Use
ReaderWriterLockSlimwhen reads vastly outnumber writes and reading causes measurable contention. - Stick to the standard
lockkeyword for short, fast operations or balanced read/write profiles. - Always release acquired locks inside
finallyblocks. - Call
Dispose()on theReaderWriterLockSliminstance when it is no longer required.