Reputation: 218
I'm trying to offload work from my database server by introducing a cache layer for some very central functions that insert a value to a table in the database and retrieves the id. This is in a multi-threaded environment.
My first approach was:
public class Cache {
private Dictionary<string, Int64> i;
public void Init() { /* init i with values from DB */ }
public Int64 Get(string value)
lock(i) {
Int64 id;
if (cache.i.TryGetValue(value, out id))
return id;
id = /* Insert to DB and retrieve ID */
cache.i[value] = id;
return id;
}
}
This helped. However the threads still wait a lot for each other. I'd like to reduce this waiting time. My first thought was to use ConcurrentDictionary.GetOrAdd(key, valueFactory)
. This would not work because valueFactory could be called more than once.
I've wound up at this approach:
public class Cache
{
private ConcurrentDictionary<string, Int64> i;
public void Init() { /* init i with values from DB */ }
public Int64 Get(string value)
{
Int64 id;
if (i.TryGetValue(value, out id))
return id;
lock (i)
{
if (i.TryGetValue(value, out id))
return id;
id = /* Insert to DB and retrieve ID */
i.TryAdd(value, id);
return id;
}
}
Is there a better way of doing this? Is this even thread-safe?
Upvotes: 3
Views: 3728
Reputation: 3413
Just FYI, in Servy's example you get an instance of Lazy
created for every call to GetOrAdd
. Now, the magic of Lazy
still happens, and you only get one call to your Func
that creates your instance. But maybe the extra instantiations of Lazy
in the above example explain the memory increase you saw when you tried it.
If you create a "double" lambda, you don't get multiple instantiations of Lazy.
E.g. paste this into a console app and compare the implementation with and without the x => new Lazy...
below:
public static class LazyEvaluationTesting
{
private static readonly ConcurrentDictionary<int, CustomLazy<CacheableItem>>
cacheableItemCache = new ConcurrentDictionary<int, CustomLazy<CacheableItem>>();
private static CacheableItem RetrieveCacheableItem(int itemId)
{
Console.WriteLine("--RETRIEVE called\t ItemId [{0}] ThreadId [{1}]", itemId, Thread.CurrentThread.ManagedThreadId);
return new CacheableItem
{
ItemId = itemId
};
}
private static void GetCacheableItem(int itemId)
{
Console.WriteLine("GET called\t ItemId [{0}] ThreadId [{1}]", itemId, Thread.CurrentThread.ManagedThreadId);
CacheableItem cacheableItem = cacheableItemCache
.GetOrAdd(itemId,
x => new CustomLazy<CacheableItem>(
() => RetrieveCacheableItem(itemId)
)
).Value;
//CacheableItem cacheableItem2 = cacheableItemCache
// .GetOrAdd(itemId,
// new CustomLazy<CacheableItem>(
// () => RetrieveCacheableItem(itemId)
// )
// ).Value;
}
public static void TestLazyEvaluation()
{
int[] itemIds = { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 };
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = 75
};
Parallel.ForEach(itemIds, options, itemId =>
{
GetCacheableItem(itemId);
GetCacheableItem(itemId);
GetCacheableItem(itemId);
GetCacheableItem(itemId);
GetCacheableItem(itemId);
});
}
private class CustomLazy<T> : Lazy<T> where T : class
{
public CustomLazy(Func<T> valueFactory)
: base(valueFactory)
{
Console.WriteLine("-Lazy Constructor called ThreadId [{0}]", Thread.CurrentThread.ManagedThreadId);
}
}
private class CacheableItem
{
public int ItemId { get; set; }
}
}
Source: Reed Copsey's Blog
Upvotes: 1
Reputation: 203827
What you're trying to do is lazily create an object that needs to be created no more than once and then accessed by any number of threads once created. Lazy
is designed for exactly this:
public class Cache
{
private ConcurrentDictionary<string, Lazy<long>> i;
public void Init() { /* init i with values from DB */ }
public Int64 Get(string value)
{
return i.GetOrAdd(value, new Lazy<long>(() =>
CreateDatabaseRecordAndGetId()))
.Value;
}
private long CreateDatabaseRecordAndGetId()
{
throw new NotImplementedException();
}
}
Upvotes: 12