|
|
- 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
- {
- /// <summary>
- /// A dictionary-like type intended for thread-safe value caches whose values are created only once ever.
- /// </summary>
- /// <typeparam name="TKey">the key type of the cache</typeparam>
- /// <typeparam name="TValue">the value type of the cache</typeparam>
- /// <remarks>
- /// This object basically wraps a <see cref="ConcurrentDictionary{TKey, TValue}"/> with some special handling
- /// to ensure that values are only created once ever, without having multiple parallel constructions.
- /// </remarks>
- public class SingleCreationValueCache<TKey, TValue>
- {
- private readonly ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)> dict;
-
- private static KeyValuePair<TKey, (ManualResetEventSlim, TValue)> ExpandKeyValuePair(KeyValuePair<TKey, TValue> kvp)
- => new KeyValuePair<TKey, (ManualResetEventSlim, TValue)>(kvp.Key, (null, kvp.Value));
- private static KeyValuePair<TKey, TValue> CompressKeyValuePair(KeyValuePair<TKey, (ManualResetEventSlim, TValue value)> kvp)
- => new KeyValuePair<TKey, TValue>(kvp.Key, kvp.Value.value);
-
- #region Constructors
- /// <summary>
- /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
- /// class that is empty, has the default concurrency level, has the default initial
- /// capacity, and uses the default comparer for the key type.
- /// </summary>
- public SingleCreationValueCache()
- => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>();
- /// <summary>
- /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
- /// class that contains elements copied from the specified <see cref="IEnumerable{T}"/>,
- /// has the default concurrency level, has the default initial capacity, and uses
- /// the default comparer for the key type.
- /// </summary>
- /// <param name="collection">the <see cref="IEnumerable{T}"/> whose element are to be used for the new cache</param>
- /// <exception cref="ArgumentNullException">when any arguments are null</exception>
- /// <exception cref="ArgumentException"><paramref name="collection"/> contains duplicate keys</exception>
- public SingleCreationValueCache(IEnumerable<KeyValuePair<TKey, TValue>> collection)
- => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(collection.Select(ExpandKeyValuePair));
- /// <summary>
- /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
- /// class that is empty, has the default concurrency level and capacity, and uses
- /// the specified <see cref="IEqualityComparer{T}"/>.
- /// </summary>
- /// <param name="comparer">the equality comparer to use when comparing keys</param>
- /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null</exception>
- public SingleCreationValueCache(IEqualityComparer<TKey> comparer)
- => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(comparer);
- /// <summary>
- /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
- /// class that contains elements copied from the specified <see cref="IEnumerable{T}"/>
- /// has the default concurrency level, has the default initial capacity, and uses
- /// the specified <see cref="IEqualityComparer{T}"/>.
- /// </summary>
- /// <param name="collection">the <see cref="IEnumerable{T}"/> whose elements are to be used for the new cache</param>
- /// <param name="comparer">the equality comparer to use when comparing keys</param>
- /// <exception cref="ArgumentNullException"><paramref name="collection"/> or <paramref name="comparer"/> is null</exception>
- public SingleCreationValueCache(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer)
- => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(collection.Select(ExpandKeyValuePair), comparer);
- #endregion
-
- /// <summary>
- /// Gets a value that indicates whether this cache is empty.
- /// </summary>
- public bool IsEmpty => dict.IsEmpty;
- /// <summary>
- /// Gets the number of elements that this cache contains.
- /// </summary>
- public int Count => dict.Count;
-
- /// <summary>
- /// Clears the cache.
- /// </summary>
- public void Clear() => dict.Clear();
- /// <summary>
- /// Gets a value indicating whether or not this cache contains <paramref name="key"/>.
- /// </summary>
- /// <param name="key">the key to search for</param>
- /// <returns><see langword="true"/> if the cache contains the key, <see langword="false"/> otherwise</returns>
- public bool ContainsKey(TKey key) => dict.ContainsKey(key);
- /// <summary>
- /// Copies the key-value pairs stored by the cache to a new array, filtering all elements that are currently being
- /// created.
- /// </summary>
- /// <returns>an array containing a snapshot of the key-value pairs contained in this cache</returns>
- public KeyValuePair<TKey, TValue>[] ToArray()
- => dict.ToArray().Where(k => k.Value.wh == null).Select(CompressKeyValuePair).ToArray();
-
- /// <summary>
- /// Attempts to get the value associated with the specified key from the cache.
- /// </summary>
- /// <param name="key">the key to search for</param>
- /// <param name="value">the value retrieved, if any</param>
- /// <returns><see langword="true"/> if the value was found, <see langword="false"/> otherwise</returns>
- 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;
- }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="key">the key to search for</param>
- /// <param name="creator">the delegate to use to create the value if it does not exist</param>
- /// <returns>the value that was found, or the result of <paramref name="creator"/></returns>
- public TValue GetOrAdd(TKey key, Func<TKey, TValue> 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
- TValue val;
- try
- {
- val = creator(key);
- while (!dict.TryUpdate(key, (null, val), cmp))
- throw new InvalidOperationException();
- }
- catch
- {
- dict.TryRemove(key, out _);
- throw;
- }
- finally
- {
- wh.Set();
- }
- return val;
- }
- }
- }
- }
|