Reputation:
I'm new in C# and trying to understand how to work with Lazy
.
I need to handle concurrent request by waiting the result of an already running operation. Requests for data may come in simultaneously with same/different credentials.
For each unique set of credentials there can be at most one GetDataInternal call in progress, with the result from that one call returned to all queued waiters when it is ready
private readonly ConcurrentDictionary<Credential, Lazy<Data>> Cache
= new ConcurrentDictionary<Credential, Lazy<Data>>();
public Data GetData(Credential credential)
{
// This instance will be thrown away if a cached
// value with our "credential" key already exists.
Lazy<Data> newLazy = new Lazy<Data>(
() => GetDataInternal(credential),
LazyThreadSafetyMode.ExecutionAndPublication
);
Lazy<Data> lazy = Cache.GetOrAdd(credential, newLazy);
bool added = ReferenceEquals(newLazy, lazy); // If true, we won the race.
Data data;
try
{
// Wait for the GetDataInternal call to complete.
data = lazy.Value;
}
finally
{
// Only the thread which created the cache value
// is allowed to remove it, to prevent races.
if (added) {
Cache.TryRemove(credential, out lazy);
}
}
return data;
}
Is that right way to use Lazy
or my code is not safe?
Update:
Is it good idea to start using MemoryCache
instead of ConcurrentDictionary
? If yes, how to create a key value, because it's a string
inside MemoryCache.Default.AddOrGetExisting()
Upvotes: 12
Views: 1918
Reputation: 149628
This answer is directed to the updated part of the original question. See @usr answer regarding thread-safety with Lazy<T>
and the potential pitfalls.
I would like to know how to avoid using
ConcurrentDictionary<TKey, TValue>
and start using MemoryCache? How to implementMemoryCache.Default.AddOrGetExisting()
?
If you're looking for a cache which has a mechanism for auto expiry, then MemoryCache
is a good choice if you don't want to implement the mechanics yourself.
In order to utilize MemoryCache
which forces a string representation for a key, you'll need to create a unique string representation of a credential, perhaps a given user id or a unique username?
If you can, you can create an override of ToString
which represents your unique identifier or simply use the said property, and utilize MemoryCache
like this:
public class Credential
{
public Credential(int userId)
{
UserId = userId;
}
public int UserId { get; private set; }
}
And now your method will look like this:
private const EvictionIntervalMinutes = 10;
public Data GetData(Credential credential)
{
Lazy<Data> newLazy = new Lazy<Data>(
() => GetDataInternal(credential), LazyThreadSafetyMode.ExecutionAndPublication);
CacheItemPolicy evictionPolicy = new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(EvictionIntervalMinutes)
};
var result = MemoryCache.Default.AddOrGetExisting(
new CacheItem(credential.UserId.ToString(), newLazy), evictionPolicy);
return result != null ? ((Lazy<Data>)result.Value).Value : newLazy.Value;
}
MemoryCache
provides you with a thread-safe implementation, this means that two threads accessing AddOrGetExisting
will only cause a single cache item to be added or retrieved. Further, Lazy<T>
with ExecutionAndPublication
guarantess only a single unique invocation of the factory method.
Upvotes: 1
Reputation: 171246
This is correct. This is a standard pattern (except for the removal) and it's a really good cache because it prevents cache stampeding.
I'm not sure you want to remove from the cache when the computation is done because the computation will be redone over and over that way. If you don't need the removal you can simplify the code by basically deleting the second half.
Note, that Lazy
has a problem in the case of an exception: The exception is stored and the factory will never be re-executed. The problem persists forever (until a human restarts the app). In my mind this makes Lazy
completely unsuitable for production use in most cases.
This means that a transient error such as a network issue can render the app unavailable permanently.
Upvotes: 6