To prove that a race condition cannot occur, we can use a callback.
public class Cache<TKey, TValue>
{
private TimeSpan _expiry;
private Dictionary<TKey, CachedObject<TValue>> _store =
new Dictionary<TKey, CachedObject<TValue>>();
public Cache(TimeSpan expiry)
{
_expiry = expiry;
}
public TValue Get(TKey key, Func<TKey, TValue> fetch)
{
lock (_store)
{
CachedObject<TValue> found;
// If the object is not in the cache, or it is expired,
// fetch it and add it to the cache.
DateTime now = DateTime.Now;
if (!_store.TryGetValue(key, out found) ||
found.DateCached + _expiry >= now)
{
found = new CachedObject<TValue>(fetch(key), now);
_store.Add(key, found);
}
return found.Value;
}
}
}
This version of the cache takes a callback to fetch the value if it is not found. Synchronization is now built into the Get method. The caller doesn't need to synchronize around it. More importantly, we can prove the correctness of the code without seeing the caller. There is no way to get it wrong.
This cache still has problems. The lock is to broad. In preventing a race condition on two thread getting the same value, we've inadvertently blocked two threads getting different values. We'll fix that next.