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; + } + } + } +}