using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; namespace IPA.Utilities.Async { /// /// A dictionary-like type intended for thread-safe value caches whose values are created only once ever. /// /// the key type of the cache /// the value type of the cache /// /// This object basically wraps a with some special handling /// to ensure that values are only created once ever, without having multiple parallel constructions. /// public class SingleCreationValueCache { private readonly ConcurrentDictionary dict; private static KeyValuePair ExpandKeyValuePair(KeyValuePair kvp) => new KeyValuePair(kvp.Key, (null, kvp.Value)); private static KeyValuePair CompressKeyValuePair(KeyValuePair kvp) => new KeyValuePair(kvp.Key, kvp.Value.value); #region Constructors /// /// Initializes a new instance of the /// class that is empty, has the default concurrency level, has the default initial /// capacity, and uses the default comparer for the key type. /// public SingleCreationValueCache() => dict = new ConcurrentDictionary(); /// /// Initializes a new instance of the /// class that contains elements copied from the specified , /// has the default concurrency level, has the default initial capacity, and uses /// the default comparer for the key type. /// /// the whose element are to be used for the new cache /// when any arguments are null /// contains duplicate keys public SingleCreationValueCache(IEnumerable> collection) => dict = new ConcurrentDictionary(collection.Select(ExpandKeyValuePair)); /// /// Initializes a new instance of the /// class that is empty, has the default concurrency level and capacity, and uses /// the specified . /// /// the equality comparer to use when comparing keys /// is null public SingleCreationValueCache(IEqualityComparer comparer) => dict = new ConcurrentDictionary(comparer); /// /// Initializes a new instance of the /// class that contains elements copied from the specified /// has the default concurrency level, has the default initial capacity, and uses /// the specified . /// /// the whose elements are to be used for the new cache /// the equality comparer to use when comparing keys /// or is null public SingleCreationValueCache(IEnumerable> collection, IEqualityComparer comparer) => dict = new ConcurrentDictionary(collection.Select(ExpandKeyValuePair), comparer); #endregion /// /// Gets a value that indicates whether this cache is empty. /// public bool IsEmpty => dict.IsEmpty; /// /// Gets the number of elements that this cache contains. /// public int Count => dict.Count; /// /// Clears the cache. /// public void Clear() => dict.Clear(); /// /// Gets a value indicating whether or not this cache contains . /// /// the key to search for /// if the cache contains the key, otherwise public bool ContainsKey(TKey key) => dict.ContainsKey(key); /// /// Copies the key-value pairs stored by the cache to a new array, filtering all elements that are currently being /// created. /// /// an array containing a snapshot of the key-value pairs contained in this cache public KeyValuePair[] ToArray() => dict.ToArray().Where(k => k.Value.wh == null).Select(CompressKeyValuePair).ToArray(); /// /// Attempts to get the value associated with the specified key from the cache. /// /// the key to search for /// the value retrieved, if any /// if the value was found, otherwise public bool TryGetValue(TKey key, out TValue value) { if (dict.TryGetValue(key, out var pair) && pair.wh != null) { value = pair.val; return true; } value = default; return false; } /// /// Gets the value associated with the specified key from the cache. If it does not exist, and /// no creators are currently running for this key, then the creator is called to create the value /// and the value is added to the cache. If there is a creator currently running for the key, then /// this waits for the creator to finish and retrieves the value. /// /// the key to search for /// the delegate to use to create the value if it does not exist /// the value that was found, or the result of public TValue GetOrAdd(TKey key, Func creator) { retry: if (dict.TryGetValue(key, out var value)) { if (value.wh != null) { value.wh.Wait(); goto retry; // this isn't really a good candidate for a loop // the loop condition will never be hit, and this should only // jump back to the beginning in exceptional situations } return value.val; } else { var wh = new ManualResetEventSlim(false); var cmp = (wh, default(TValue)); if (!dict.TryAdd(key, cmp)) goto retry; // someone else beat us to the punch, retry getting their value and wait for them var val = creator(key); while (!dict.TryUpdate(key, (null, val), cmp)) throw new InvalidOperationException(); wh.Set(); return val; } } } }