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