You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
8.2 KiB

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Concurrent;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Text;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. namespace IPA.Utilities.Async
  12. {
  13. /// <summary>
  14. /// A dictionary-like type intended for thread-safe value caches whose values are created only once ever.
  15. /// </summary>
  16. /// <typeparam name="TKey">the key type of the cache</typeparam>
  17. /// <typeparam name="TValue">the value type of the cache</typeparam>
  18. /// <remarks>
  19. /// This object basically wraps a <see cref="ConcurrentDictionary{TKey, TValue}"/> with some special handling
  20. /// to ensure that values are only created once ever, without having multiple parallel constructions.
  21. /// </remarks>
  22. public class SingleCreationValueCache<TKey, TValue>
  23. {
  24. private readonly ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)> dict;
  25. private static KeyValuePair<TKey, (ManualResetEventSlim, TValue)> ExpandKeyValuePair(KeyValuePair<TKey, TValue> kvp)
  26. => new KeyValuePair<TKey, (ManualResetEventSlim, TValue)>(kvp.Key, (null, kvp.Value));
  27. private static KeyValuePair<TKey, TValue> CompressKeyValuePair(KeyValuePair<TKey, (ManualResetEventSlim, TValue value)> kvp)
  28. => new KeyValuePair<TKey, TValue>(kvp.Key, kvp.Value.value);
  29. #region Constructors
  30. /// <summary>
  31. /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
  32. /// class that is empty, has the default concurrency level, has the default initial
  33. /// capacity, and uses the default comparer for the key type.
  34. /// </summary>
  35. public SingleCreationValueCache()
  36. => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>();
  37. /// <summary>
  38. /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
  39. /// class that contains elements copied from the specified <see cref="IEnumerable{T}"/>,
  40. /// has the default concurrency level, has the default initial capacity, and uses
  41. /// the default comparer for the key type.
  42. /// </summary>
  43. /// <param name="collection">the <see cref="IEnumerable{T}"/> whose element are to be used for the new cache</param>
  44. /// <exception cref="ArgumentNullException">when any arguments are null</exception>
  45. /// <exception cref="ArgumentException"><paramref name="collection"/> contains duplicate keys</exception>
  46. public SingleCreationValueCache(IEnumerable<KeyValuePair<TKey, TValue>> collection)
  47. => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(collection.Select(ExpandKeyValuePair));
  48. /// <summary>
  49. /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
  50. /// class that is empty, has the default concurrency level and capacity, and uses
  51. /// the specified <see cref="IEqualityComparer{T}"/>.
  52. /// </summary>
  53. /// <param name="comparer">the equality comparer to use when comparing keys</param>
  54. /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null</exception>
  55. public SingleCreationValueCache(IEqualityComparer<TKey> comparer)
  56. => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(comparer);
  57. /// <summary>
  58. /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
  59. /// class that contains elements copied from the specified <see cref="IEnumerable{T}"/>
  60. /// has the default concurrency level, has the default initial capacity, and uses
  61. /// the specified <see cref="IEqualityComparer{T}"/>.
  62. /// </summary>
  63. /// <param name="collection">the <see cref="IEnumerable{T}"/> whose elements are to be used for the new cache</param>
  64. /// <param name="comparer">the equality comparer to use when comparing keys</param>
  65. /// <exception cref="ArgumentNullException"><paramref name="collection"/> or <paramref name="comparer"/> is null</exception>
  66. public SingleCreationValueCache(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer)
  67. => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(collection.Select(ExpandKeyValuePair), comparer);
  68. #endregion
  69. /// <summary>
  70. /// Gets a value that indicates whether this cache is empty.
  71. /// </summary>
  72. public bool IsEmpty => dict.IsEmpty;
  73. /// <summary>
  74. /// Gets the number of elements that this cache contains.
  75. /// </summary>
  76. public int Count => dict.Count;
  77. /// <summary>
  78. /// Clears the cache.
  79. /// </summary>
  80. public void Clear() => dict.Clear();
  81. /// <summary>
  82. /// Gets a value indicating whether or not this cache contains <paramref name="key"/>.
  83. /// </summary>
  84. /// <param name="key">the key to search for</param>
  85. /// <returns><see langword="true"/> if the cache contains the key, <see langword="false"/> otherwise</returns>
  86. public bool ContainsKey(TKey key) => dict.ContainsKey(key);
  87. /// <summary>
  88. /// Copies the key-value pairs stored by the cache to a new array, filtering all elements that are currently being
  89. /// created.
  90. /// </summary>
  91. /// <returns>an array containing a snapshot of the key-value pairs contained in this cache</returns>
  92. public KeyValuePair<TKey, TValue>[] ToArray()
  93. => dict.ToArray().Where(k => k.Value.wh == null).Select(CompressKeyValuePair).ToArray();
  94. /// <summary>
  95. /// Attempts to get the value associated with the specified key from the cache.
  96. /// </summary>
  97. /// <param name="key">the key to search for</param>
  98. /// <param name="value">the value retrieved, if any</param>
  99. /// <returns><see langword="true"/> if the value was found, <see langword="false"/> otherwise</returns>
  100. public bool TryGetValue(TKey key, out TValue value)
  101. {
  102. if (dict.TryGetValue(key, out var pair) && pair.wh != null)
  103. {
  104. value = pair.val;
  105. return true;
  106. }
  107. value = default;
  108. return false;
  109. }
  110. /// <summary>
  111. /// Gets the value associated with the specified key from the cache. If it does not exist, and
  112. /// no creators are currently running for this key, then the creator is called to create the value
  113. /// and the value is added to the cache. If there is a creator currently running for the key, then
  114. /// this waits for the creator to finish and retrieves the value.
  115. /// </summary>
  116. /// <param name="key">the key to search for</param>
  117. /// <param name="creator">the delegate to use to create the value if it does not exist</param>
  118. /// <returns>the value that was found, or the result of <paramref name="creator"/></returns>
  119. public TValue GetOrAdd(TKey key, Func<TKey, TValue> creator)
  120. {
  121. retry:
  122. if (dict.TryGetValue(key, out var value))
  123. {
  124. if (value.wh != null)
  125. {
  126. value.wh.Wait();
  127. goto retry; // this isn't really a good candidate for a loop
  128. // the loop condition will never be hit, and this should only
  129. // jump back to the beginning in exceptional situations
  130. }
  131. return value.val;
  132. }
  133. else
  134. {
  135. var wh = new ManualResetEventSlim(false);
  136. var cmp = (wh, default(TValue));
  137. if (!dict.TryAdd(key, cmp))
  138. goto retry; // someone else beat us to the punch, retry getting their value and wait for them
  139. var val = creator(key);
  140. while (!dict.TryUpdate(key, (null, val), cmp))
  141. throw new InvalidOperationException();
  142. wh.Set();
  143. return val;
  144. }
  145. }
  146. }
  147. }