I have a static dictionary that I want to use as an internal cache for an ASP.NET application. The number of reads will greatly outnumber the number of writes, and I'd like to make sure I do this in a thread-safe manner without unnecessarily hurting performance with extraneous locks.
I have two implementations - the first uses a simple lock object and the lock
keywork, and the second uses a ReadWriteLockSlim
.
Standard locking
public class ConcurrentCache
{
private static readonly object LocationsLock = new object();
private static bool _locationsLoaded = false;
private static readonly ConcurrentDictionary<long, Location> Locations =
new ConcurrentDictionary<long, Location>();
public Location Get(long id)
{
EnsureLocationsDictionaryIsPopulated();
return Locations[id];
}
private void EnsureLocationsDictionaryIsPopulated()
{
if (Locations.Count > 0 || _locationsLoaded) return;
// Still locking, even though I'm using a ConcurrentDictionary,
// so that all locations are loaded only once and I don't have
// to worry about locking the reads.
lock (LocationsLock)
{
if (Locations.Count > 0 || _locationsLoaded) return;
PopulateLocationsDictionary();
_locationsLoaded = true;
}
}
// see below for other methods
}
ReadWriteLockSlim locking
public class ReadWriteCache
{
private static readonly ReaderWriterLockSlim LockSlim =
new ReaderWriterLockSlim();
private static bool _locationsLoaded = false;
private static readonly Dictionary<long, Location> Locations =
new Dictionary<long, Location>();
public Location Get(long id)
{
EnsureLocationsDictionaryIsPopulated();
return Locations[id];
}
private void EnsureLocationsDictionaryIsPopulated()
{
if (Locations.Count > 0 || _locationsLoaded) return;
LockSlim.EnterWriteLock();
try
{
if (Locations.Count > 0 || _locationsLoaded) return;
PopulateLocationsDictionary();
_locationsLoaded = true;
}
finally
{
LockSlim.ExitWriteLock();
}
}
// see below for other methods
}
Both classes have the same two methods:
private void PopulateLocationsDictionary()
{
var items = LoadAllLocationsFromExternalSource();
if (items == null || items.Count == 0) return;
for (int i = 0; i < items.Count; i++)
{
var location = items[i];
Locations[location.Id] = location;
}
}
/// <summary>
/// This method actually calls an external API and takes
/// at least 5 seconds to run.
/// </summary>
/// <returns></returns>
private List<Location> LoadAllLocationsFromExternalSource()
{
return new List<Location>
{
new Location
{Id = 5, Value1 = "one", Value2 = "two", Value3 = "three"},
new Location
{Id = 10, Value1 = "I", Value2 = "II", Value3 = "III"},
new Location
{Id = 42, Value1 = "un", Value2 = "deux", Value3 = "trois"}
};
}
I've seen from this post (When is ReaderWriterLockSlim better than a simple lock?) that the ReadWriteLockSlim
is expected to outperform a standard lock when the access pattern involves mostly reads. Is that still the case in my two scenarios? How does the ReadWriteLockSlim
compare to a ConcurrentDictionary
? Are there any other considerations I'm not taking into account?