diff --git a/FastCache/FastCache.cs b/FastCache/FastCache.cs
index 79eb1b3..bfd1b09 100644
--- a/FastCache/FastCache.cs
+++ b/FastCache/FastCache.cs
@@ -1,31 +1,31 @@
-using System;
-using System.Collections;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Threading;
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
-namespace Jitbit.Utils
-{
- ///
- /// faster MemoryCache alternative. basically a concurrent dictionary with expiration
- ///
- public class FastCache : IEnumerable>, IDisposable
- {
- private readonly ConcurrentDictionary _dict = new ConcurrentDictionary();
-
+namespace Jitbit.Utils
+{
+ ///
+ /// faster MemoryCache alternative. basically a concurrent dictionary with expiration
+ ///
+ public class FastCache : IEnumerable>, IDisposable
+ {
+ private readonly ConcurrentDictionary _dict = new ConcurrentDictionary();
+
private readonly Timer _cleanUpTimer;
- ///
- /// Initializes a new empty instance of
- ///
- /// cleanup interval in milliseconds, default is 10000
- public FastCache(int cleanupJobInterval = 10000)
- {
- _cleanUpTimer = new Timer(s => { _ = EvictExpiredJob(); }, null, cleanupJobInterval, cleanupJobInterval);
- }
-
- private static SemaphoreSlim _globalStaticLock = new(1);
+ ///
+ /// Initializes a new empty instance of
+ ///
+ /// cleanup interval in milliseconds, default is 10000
+ public FastCache(int cleanupJobInterval = 10000)
+ {
+ _cleanUpTimer = new Timer(s => { _ = EvictExpiredJob(); }, null, cleanupJobInterval, cleanupJobInterval);
+ }
+
+ private static SemaphoreSlim _globalStaticLock = new(1);
private async Task EvictExpiredJob()
{
//if an applicaiton has many-many instances of FastCache objects, make sure the timer-based
@@ -43,230 +43,230 @@ await _globalStaticLock.WaitAsync()
EvictExpired();
}
finally { _globalStaticLock.Release(); }
- }
-
- ///
- /// Cleans up expired items (dont' wait for the background job)
- /// There's rarely a need to execute this method, b/c getting an item checks TTL anyway.
- ///
- public void EvictExpired()
- {
- //Eviction already started by another thread? forget it, lets move on
- if (Monitor.TryEnter(_cleanUpTimer)) //use the timer-object for our lock, it's local, private and instance-type, so its ok
- {
- try
- {
- //cache current tick count in a var to prevent calling it every iteration inside "IsExpired()" in a tight loop.
- //On a 10000-items cache this allows us to slice 30 microseconds: 330 vs 360 microseconds which is 10% faster
- //On a 50000-items cache it's even more: 2.057ms vs 2.817ms which is 35% faster!!
- //the bigger the cache the bigger the win
- var currTime = Environment.TickCount64;
-
- foreach (var p in _dict)
- {
- if (currTime > p.Value.TickCountWhenToKill) //instead of calling "p.Value.IsExpired" we're essentially doing the same thing manually
- _dict.TryRemove(p);
- }
- }
- finally
- {
- Monitor.Exit(_cleanUpTimer);
- }
- }
- }
-
- ///
- /// Returns total count, including expired items too, if they were not yet cleaned by the eviction job
- ///
- public int Count => _dict.Count;
-
- ///
- /// Removes all items from the cache
- ///
- public void Clear() => _dict.Clear();
-
- ///
- /// Adds an item to cache if it does not exist, updates the existing item otherwise. Updating an item resets its TTL.
- ///
- /// The key to add
- /// The value to add
- /// TTL of the item
- public void AddOrUpdate(TKey key, TValue value, TimeSpan ttl)
- {
- var ttlValue = new TtlValue(value, ttl);
-
- _dict.AddOrUpdate(key, (k, c) => c, (k, v, c) => c, ttlValue);
- }
-
- ///
- /// Attempts to get a value by key
- ///
- /// The key to get
- /// When method returns, contains the object with the key if found, otherwise default value of the type
- /// True if value exists, otherwise false
- public bool TryGet(TKey key, out TValue value)
- {
- value = default(TValue);
-
- if (!_dict.TryGetValue(key, out TtlValue ttlValue))
- return false; //not found
-
- if (ttlValue.IsExpired()) //found but expired
- {
- var kv = new KeyValuePair(key, ttlValue);
-
- //secret atomic removal method (only if both key and value match condition
- //https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
- //so that we don't need any locks!! woohoo
- _dict.TryRemove(kv);
-
- /* EXPLANATION:
- * when an item was "found but is expired" - we need to treat as "not found" and discard it.
- * One solution is to use a lock
- * so that the the three steps "exist? expired? remove!" are performed atomically.
- * Otherwise another tread might chip in, and ADD a non-expired item with the same key while we're evicting it.
- * And we'll be removing a non-expired key taht was just added
- *
- * BUT instead of using locks we can remove by key AND value. So if another thread has just rushed in
- * and added another item with the same key - that other item won't be removed.
- *
- * basically, instead of doing this
- *
- * lock {
- * exists?
- * expired?
- * remove by key!
- * }
- *
- * we do this
- *
- * exists? (if yes returns the value)
- * expired?
- * remove by key AND value
- *
- * If another thread has modified the value - it won't remove it.
- *
- * Locks suck becasue add extra 50ns to benchmark, so it becomes 110ns instead of 70ns which sucks.
- * So - no locks then!!!
- *
- * */
-
- return false;
- }
-
- value = ttlValue.Value;
- return true;
- }
-
- ///
- /// Attempts to add a key/value item
- ///
- /// The key to add
- /// The value to add
- /// TTL of the item
- /// True if value was added, otherwise false (already exists)
- public bool TryAdd(TKey key, TValue value, TimeSpan ttl)
- {
- if (TryGet(key, out _))
- return false;
-
- return _dict.TryAdd(key, new TtlValue(value, ttl));
- }
-
- ///
- /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
- ///
- /// The key to add
- /// The factory function used to generate the item for the key
- /// TTL of the item
- public TValue GetOrAdd(TKey key, Func valueFactory, TimeSpan ttl)
- {
- if (TryGet(key, out var value))
- return value;
-
- return _dict.GetOrAdd(key, (k, v) => new TtlValue(v.valueFactory(k), v.ttl), (ttl, valueFactory)).Value;
- }
-
- ///
- /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
- ///
- /// The key to add
- /// The value to add
- /// TTL of the item
- public TValue GetOrAdd(TKey key, TValue value, TimeSpan ttl)
- {
- if (TryGet(key, out var existingValue))
- return existingValue;
-
- return _dict.GetOrAdd(key, new TtlValue(value, ttl)).Value;
- }
-
- ///
- /// Tries to remove item with the specified key
- ///
- /// The key of the element to remove
- public void Remove(TKey key)
- {
- _dict.TryRemove(key, out _);
- }
-
- ///
- /// Tries to remove item with the specified key, also returns the object removed in an "out" var
- ///
- /// The key of the element to remove
- /// Contains the object removed or the default value if not found
- public bool TryRemove(TKey key, out TValue value)
- {
- bool res = _dict.TryRemove(key, out var ttlValue) && !ttlValue.IsExpired();
- value = res ? ttlValue.Value : default(TValue);
- return res;
- }
-
- public IEnumerator> GetEnumerator()
- {
- foreach (var kvp in _dict)
- {
- if (!kvp.Value.IsExpired())
- yield return new KeyValuePair(kvp.Key, kvp.Value.Value);
- }
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return this.GetEnumerator();
- }
-
- private class TtlValue
- {
- public readonly TValue Value;
- public readonly long TickCountWhenToKill;
-
- public TtlValue(TValue value, TimeSpan ttl)
- {
- Value = value;
- TickCountWhenToKill = Environment.TickCount64 + (long)ttl.TotalMilliseconds;
- }
-
- public bool IsExpired()
- {
- return Environment.TickCount64 > TickCountWhenToKill;
- }
- }
-
- //IDispisable members
- private bool _disposedValue;
- public void Dispose() => Dispose(true);
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposedValue)
- {
+ }
+
+ ///
+ /// Cleans up expired items (dont' wait for the background job)
+ /// There's rarely a need to execute this method, b/c getting an item checks TTL anyway.
+ ///
+ public void EvictExpired()
+ {
+ //Eviction already started by another thread? forget it, lets move on
+ if (Monitor.TryEnter(_cleanUpTimer)) //use the timer-object for our lock, it's local, private and instance-type, so its ok
+ {
+ try
+ {
+ //cache current tick count in a var to prevent calling it every iteration inside "IsExpired()" in a tight loop.
+ //On a 10000-items cache this allows us to slice 30 microseconds: 330 vs 360 microseconds which is 10% faster
+ //On a 50000-items cache it's even more: 2.057ms vs 2.817ms which is 35% faster!!
+ //the bigger the cache the bigger the win
+ var currTime = Environment.TickCount64;
+
+ foreach (var p in _dict)
+ {
+ if (currTime > p.Value.TickCountWhenToKill) //instead of calling "p.Value.IsExpired" we're essentially doing the same thing manually
+ _dict.TryRemove(p);
+ }
+ }
+ finally
+ {
+ Monitor.Exit(_cleanUpTimer);
+ }
+ }
+ }
+
+ ///
+ /// Returns total count, including expired items too, if they were not yet cleaned by the eviction job
+ ///
+ public int Count => _dict.Count;
+
+ ///
+ /// Removes all items from the cache
+ ///
+ public void Clear() => _dict.Clear();
+
+ ///
+ /// Adds an item to cache if it does not exist, updates the existing item otherwise. Updating an item resets its TTL.
+ ///
+ /// The key to add
+ /// The value to add
+ /// TTL of the item
+ public void AddOrUpdate(TKey key, TValue value, TimeSpan ttl)
+ {
+ var ttlValue = new TtlValue(value, ttl);
+
+ _dict.AddOrUpdate(key, (k, c) => c, (k, v, c) => c, ttlValue);
+ }
+
+ ///
+ /// Attempts to get a value by key
+ ///
+ /// The key to get
+ /// When method returns, contains the object with the key if found, otherwise default value of the type
+ /// True if value exists, otherwise false
+ public bool TryGet(TKey key, out TValue value)
+ {
+ value = default(TValue);
+
+ if (!_dict.TryGetValue(key, out TtlValue ttlValue))
+ return false; //not found
+
+ if (ttlValue.IsExpired()) //found but expired
+ {
+ var kv = new KeyValuePair(key, ttlValue);
+
+ //secret atomic removal method (only if both key and value match condition
+ //https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
+ //so that we don't need any locks!! woohoo
+ _dict.TryRemove(kv);
+
+ /* EXPLANATION:
+ * when an item was "found but is expired" - we need to treat as "not found" and discard it.
+ * One solution is to use a lock
+ * so that the three steps "exist? expired? remove!" are performed atomically.
+ * Otherwise another tread might chip in, and ADD a non-expired item with the same key while we're evicting it.
+ * And we'll be removing a non-expired key that was just added.
+ *
+ * BUT instead of using locks we can remove by key AND value. So if another thread has just rushed in
+ * and added another item with the same key - that other item won't be removed.
+ *
+ * basically, instead of doing this
+ *
+ * lock {
+ * exists?
+ * expired?
+ * remove by key!
+ * }
+ *
+ * we do this
+ *
+ * exists? (if yes returns the value)
+ * expired?
+ * remove by key AND value
+ *
+ * If another thread has modified the value - it won't remove it.
+ *
+ * Locks suck becasue add extra 50ns to benchmark, so it becomes 110ns instead of 70ns which sucks.
+ * So - no locks then!!!
+ *
+ * */
+
+ return false;
+ }
+
+ value = ttlValue.Value;
+ return true;
+ }
+
+ ///
+ /// Attempts to add a key/value item
+ ///
+ /// The key to add
+ /// The value to add
+ /// TTL of the item
+ /// True if value was added, otherwise false (already exists)
+ public bool TryAdd(TKey key, TValue value, TimeSpan ttl)
+ {
+ if (TryGet(key, out _))
+ return false;
+
+ return _dict.TryAdd(key, new TtlValue(value, ttl));
+ }
+
+ ///
+ /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
+ ///
+ /// The key to add
+ /// The factory function used to generate the item for the key
+ /// TTL of the item
+ public TValue GetOrAdd(TKey key, Func valueFactory, TimeSpan ttl)
+ {
+ if (TryGet(key, out var value))
+ return value;
+
+ return _dict.GetOrAdd(key, (k, v) => new TtlValue(v.valueFactory(k), v.ttl), (ttl, valueFactory)).Value;
+ }
+
+ ///
+ /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
+ ///
+ /// The key to add
+ /// The value to add
+ /// TTL of the item
+ public TValue GetOrAdd(TKey key, TValue value, TimeSpan ttl)
+ {
+ if (TryGet(key, out var existingValue))
+ return existingValue;
+
+ return _dict.GetOrAdd(key, new TtlValue(value, ttl)).Value;
+ }
+
+ ///
+ /// Tries to remove item with the specified key
+ ///
+ /// The key of the element to remove
+ public void Remove(TKey key)
+ {
+ _dict.TryRemove(key, out _);
+ }
+
+ ///
+ /// Tries to remove item with the specified key, also returns the object removed in an "out" var
+ ///
+ /// The key of the element to remove
+ /// Contains the object removed or the default value if not found
+ public bool TryRemove(TKey key, out TValue value)
+ {
+ bool res = _dict.TryRemove(key, out var ttlValue) && !ttlValue.IsExpired();
+ value = res ? ttlValue.Value : default(TValue);
+ return res;
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ foreach (var kvp in _dict)
+ {
+ if (!kvp.Value.IsExpired())
+ yield return new KeyValuePair(kvp.Key, kvp.Value.Value);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ private class TtlValue
+ {
+ public readonly TValue Value;
+ public readonly long TickCountWhenToKill;
+
+ public TtlValue(TValue value, TimeSpan ttl)
+ {
+ Value = value;
+ TickCountWhenToKill = Environment.TickCount64 + (long)ttl.TotalMilliseconds;
+ }
+
+ public bool IsExpired()
+ {
+ return Environment.TickCount64 > TickCountWhenToKill;
+ }
+ }
+
+ //IDispisable members
+ private bool _disposedValue;
+ public void Dispose() => Dispose(true);
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
if (disposing)
{
_cleanUpTimer.Dispose();
- }
-
- _disposedValue = true;
- }
- }
- }
-}
+ }
+
+ _disposedValue = true;
+ }
+ }
+ }
+}