I am currently implementing a thread-safe in-memory cache mechanism, with the intent of storing objects that are expensive to create or often used by the system.
I also want to include an expiration policy, i.e number of times an object was accessed and when it was last accessed. This will be used to scavenge, so I opted to go with a concurrent dictionary.
Below is my spiked code to get objects in a dictionary. I get an object from memory every time and increment the accessed count and date.
I'm not sure if it is thread-safe or if there are any other patterns out there I could use.
Imports System.Collections.Concurrent
Imports NUnit.Framework
Imports System.Threading
Imports CacheManagement
Imports CacheManagement.Extension
<TestFixture>
Public Class ConcurrentDictionaryTryAndGetTests
Private _cache As ConcurrentDictionary(Of Integer, CacheItem)
Private Const Key As Integer = 100
Private _data As CacheItem = Nothing
<SetUp()>
Public Sub Setup()
_data = New CacheItem With {.NumberOfTimesAccessed = 0, .Priority = CacheItemPriority.Normal}
_cache = New ConcurrentDictionary(Of Integer, CacheItem)
Dim added = _cache.TryAdd(Key, _data)
Console.WriteLine(added)
End Sub
<Test>
Public Sub ConcurrentDictionaryTests()
Dim parallelOptions = New ParallelOptions
With parallelOptions
.MaxDegreeOfParallelism = 2
End With
Parallel.For(0, 10, parallelOptions, Sub(index)
TryAndGet(_cache, Key)
End Sub)
End Sub
Private Shared Function TryAndGet(ByVal dict As ConcurrentDictionary(Of Integer, CacheItem),
ByVal key As Integer) As WaitCallback
Dim data As CacheItem = Nothing
If (dict.TryGetValue(key, data)) Then
Thread.Sleep(1000)
Interlocked.Increment(data.NumberOfTimesAccessed)
Interlocked.Exchange(data.LastUsedDateTicks, DateTime.Now.Ticks)
'note: interlock are atomic console.write is not atomic
Console.WriteLine("number of times accessed {0} at ({1})", data.NumberOfTimesAccessed, data.LastUsedDateTicks.ToDate())
Return Function(o) data
End If
Return Nothing
End Function
End Class
Retrieving data from first and second level cache:
Public Function GetDataStub(ByVal key As String) As Object
Dim sw = New Stopwatch
sw.Start()
Dim cacheData As Object = realCache.GetDataFromInMemory(key)
If cacheData Is Nothing Then cacheData = realCache.GetDataFromBackingStore(key)
logger.InfoFormat("time taken to get data:{0} - {1}", key, sw.ElapsedMilliseconds)
Return cacheData
End Function
Public Function GetDataFromInMemory(ByVal key As String) As Object
Dim cacheItem As CacheItem = Nothing
If IsInMemoryCacheEnabled = False Then Return Nothing
SyncLock inMemoryCache.SyncRoot
cacheItem = DirectCast(inMemoryCache(key), CacheItem)
If IsObjectNotInCache(cacheItem) Then Return Nothing
cacheItem.RetrievalCount += 1
cacheItem.LastAccessedTime = DateTime.Now
Return cacheItem.Value
End SyncLock
End Function
Public Function GetDataFromBackingStore(ByVal key As String) As Object
Dim cacheItem As CacheItem = Nothing
cacheItem = DirectCast(backingStore.GetData(key), CacheItem)
If IsObjectNotInCache(cacheItem) Then Return Nothing
AddToInMemoryStore(key,
cacheItem.Value,
cacheItem.ScavengingPriority,
cacheItem.GetExpirations())
Return cacheItem.Value
End Function
Private Sub AddToInMemoryStore(ByVal key As String,
ByVal value As Object,
ByVal scavengingPriority As CacheItemPriority,
ByVal ParamArray expirations As ICacheItemExpiration())
Dim cacheItem As CacheItem = Nothing
SyncLock inMemoryCache.SyncRoot
If inMemoryCache.Contains(key) Then
cacheItem = DirectCast(inMemoryCache(key), CacheItem)
Else
cacheItem = New CacheItem(key,
addInProgressFlag,
CacheItemPriority.Normal,
Nothing)
End If
cacheItem.Replace(value, scavengingPriority, expirations)
inMemoryCache(key) = cacheItem
End SyncLock
End Sub
Adding data to cache:
Public Sub Add(ByVal key As String,
ByVal value As Object,
ByVal scavengingPriority As CacheItemPriority,
ByVal ParamArray expirations As ICacheItemExpiration())
Dim cacheItem As CacheItem = Nothing
Dim retrivalCount As Integer
SyncLock inMemoryCache.SyncRoot
If inMemoryCache.Contains(key) Then
cacheItem = DirectCast(inMemoryCache(key), CacheItem)
Else
cacheItem = New CacheItem(key, addInProgressFlag, CacheItemPriority.Normal, Nothing)
End If
cacheItem.Replace(value, scavengingPriority, expirations)
inMemoryCache(key) = cacheItem
retrivalCount = cacheItem.RetrievalCount
End SyncLock
'fire and forget
Dim tmpCacheItem = New CacheItem(key, value, scavengingPriority, expirations)
tmpCacheItem.RetrievalCount = retrivalCount
ThreadPool.UnsafeQueueUserWorkItem(AddressOf AddToBackingStore, tmpCacheItem)
End Sub
ConcurrentDictionary
suggests .NET 4.0+ and there is a thread-safe MemoryCache built into the framework which comes with all sorts of expiry policy etc. Rolling your own one doesn't seem sensible in that case \$\endgroup\$