Browse Source

Made more single-creation caches thread safe

pull/46/head
Anairkoen Schno 4 years ago
parent
commit
2572cbd277
9 changed files with 235 additions and 111 deletions
  1. +13
    -13
      IPA.Loader/Config/ConfigRuntime.cs
  2. +26
    -26
      IPA.Loader/Config/Stores/CustomObjectConverter.cs
  3. +8
    -31
      IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs
  4. +4
    -6
      IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs
  5. +1
    -0
      IPA.Loader/IPA.Loader.csproj
  6. +2
    -0
      IPA.Loader/Loader/Features/Feature.cs
  7. +12
    -12
      IPA.Loader/Loader/PluginComponent.cs
  8. +15
    -23
      IPA.Loader/Utilities/Accessor.cs
  9. +154
    -0
      IPA.Loader/Utilities/Async/SingleCreationValueCache.cs

+ 13
- 13
IPA.Loader/Config/ConfigRuntime.cs View File

@ -64,19 +64,19 @@ namespace IPA.Config
=> ShutdownRuntime();
internal static void ShutdownRuntime()
{
try
{
watcherTrackConfigs.Clear();
var watchList = watchers.ToArray();
watchers.Clear();
foreach (var pair in watchList)
pair.Value.EnableRaisingEvents = false;
loadScheduler.Join(); // we can wait for the loads to finish
saveThread.Abort(); // eww, but i don't like any of the other potential solutions
SaveAll();
try
{
watcherTrackConfigs.Clear();
var watchList = watchers.ToArray();
watchers.Clear();
foreach (var pair in watchList)
pair.Value.EnableRaisingEvents = false;
loadScheduler.Join(); // we can wait for the loads to finish
saveThread.Abort(); // eww, but i don't like any of the other potential solutions
SaveAll();
}
catch
{


+ 26
- 26
IPA.Loader/Config/Stores/CustomObjectConverter.cs View File

@ -85,56 +85,56 @@ namespace IPA.Config.Stores.Converters
=> Serialize(obj, parent);
}
/// <summary>
/// A <see cref="ValueConverter{T}"/> for custom value types, serialized identically to the reference types serialized with
/// <see cref="GeneratedStore.Generated{T}(Config, bool)"/>.
/// </summary>
/// <summary>
/// A <see cref="ValueConverter{T}"/> for custom value types, serialized identically to the reference types serialized with
/// <see cref="GeneratedStore.Generated{T}(Config, bool)"/>.
/// </summary>
/// <typeparam name="T">the type of the value to convert</typeparam>
public class CustomValueTypeConverter<T> : ValueConverter<T> where T : struct
{
private static readonly GeneratedStoreImpl.SerializeObject<T> serialize
= GeneratedStoreImpl.GetSerializerDelegate<T>();
private static readonly GeneratedStoreImpl.DeserializeObject<T> deserialize
= GeneratedStoreImpl.GetDeserializerDelegate<T>();
public class CustomValueTypeConverter<T> : ValueConverter<T> where T : struct
{
private static readonly GeneratedStoreImpl.SerializeObject<T> serialize
= GeneratedStoreImpl.GetSerializerDelegate<T>();
private static readonly GeneratedStoreImpl.DeserializeObject<T> deserialize
= GeneratedStoreImpl.GetDeserializerDelegate<T>();
/// <summary>
/// Deserializes <paramref name="value"/> into a <typeparamref name="T"/> with the given <paramref name="parent"/>.
/// </summary>
/// <param name="value">the <see cref="Value"/> to deserialize</param>
/// <param name="parent">the parent object that will own the deserialized value</param>
/// <returns>the deserialized value</returns>
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public static T Deserialize(Value value, object parent)
=> deserialize(value, parent);
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public static T Deserialize(Value value, object parent)
=> deserialize(value, parent);
/// <summary>
/// Serializes <paramref name="obj"/> into a corresponding <see cref="Value"/> structure.
/// </summary>
/// <param name="obj">the object to serialize</param>
/// <returns>the <see cref="Value"/> tree that represents <paramref name="obj"/></returns>
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
public static Value Serialize(T obj)
=> serialize(obj);
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
public static Value Serialize(T obj)
=> serialize(obj);
/// <summary>
/// Deserializes <paramref name="value"/> into a <typeparamref name="T"/> with the given <paramref name="parent"/>.
/// </summary>
/// <param name="value">the <see cref="Value"/> to deserialize</param>
/// <param name="parent">the parent object that will own the deserialized value</param>
/// <returns>the deserialized value</returns>
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public override T FromValue(Value value, object parent)
=> Deserialize(value, parent);
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public override T FromValue(Value value, object parent)
=> Deserialize(value, parent);
/// <summary>
/// Serializes <paramref name="obj"/> into a <see cref="Value"/> structure, given <paramref name="parent"/>.
/// </summary>
/// <param name="obj">the object to serialize</param>
/// <param name="parent">the parent object that owns <paramref name="obj"/></param>
/// <returns>the <see cref="Value"/> tree that represents <paramref name="obj"/></returns>
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
public override Value ToValue(T obj, object parent)
=> Serialize(obj);
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
public override Value ToValue(T obj, object parent)
=> Serialize(obj);
}
}

+ 8
- 31
IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs View File

@ -16,7 +16,8 @@ using Boolean = IPA.Config.Data.Boolean;
using System.Collections;
using IPA.Utilities;
using System.ComponentModel;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using IPA.Utilities.Async;
#if NET3
using Net3_Proxy;
using Array = Net3_Proxy.Array;
@ -38,38 +39,13 @@ namespace IPA.Config.Stores
internal static T Create<T>(IGeneratedStore parent) where T : class => (T)Create(typeof(T), parent);
private static IConfigStore Create(Type type, IGeneratedStore parent)
=> GetCreator(type)(parent);
private static readonly ConcurrentDictionary<Type, (ManualResetEventSlim wh, GeneratedStoreCreator ctor, Type type)> generatedCreators
= new ConcurrentDictionary<Type, (ManualResetEventSlim wh, GeneratedStoreCreator ctor, Type type)>();
=> GetCreator(type)(parent);
private static readonly SingleCreationValueCache<Type, (GeneratedStoreCreator ctor, Type type)> generatedCreators
= new SingleCreationValueCache<Type, (GeneratedStoreCreator ctor, Type type)>();
private static (GeneratedStoreCreator ctor, Type type) GetCreatorAndGeneratedType(Type t)
{
retry:
if (generatedCreators.TryGetValue(t, out var gen))
{
if (gen.wh != null)
{
gen.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 (gen.ctor, gen.type);
}
else
{
var wh = new ManualResetEventSlim(false);
var cmp = (wh, (GeneratedStoreCreator)null, (Type)null);
if (!generatedCreators.TryAdd(t, cmp))
goto retry; // someone else beat us to the punch, retry getting their value and wait for them
var (ctor, type) = MakeCreator(t);
while (!generatedCreators.TryUpdate(t, (null, ctor, type), cmp))
throw new InvalidOperationException("Somehow, multiple MakeCreators started running for the same target type!");
wh.Set();
return (ctor, type);
}
}
=> generatedCreators.GetOrAdd(t, MakeCreator);
internal static GeneratedStoreCreator GetCreator(Type t)
=> GetCreatorAndGeneratedType(t).ctor;
@ -111,6 +87,7 @@ namespace IPA.Config.Stores
}
}
// TODO: does this need to be a SingleCreationValueCache or similar?
private static readonly Dictionary<Type, Dictionary<Type, FieldInfo>> TypeRequiredConverters = new Dictionary<Type, Dictionary<Type, FieldInfo>>();
private static void CreateAndInitializeConvertersFor(Type type, IEnumerable<SerializedMemberInfo> structure)
{


+ 4
- 6
IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs View File

@ -2,6 +2,7 @@
using IPA.Config.Stores.Converters;
using IPA.Logging;
using IPA.Utilities;
using IPA.Utilities.Async;
using System;
using System.Collections.Generic;
using System.Linq;
@ -127,14 +128,11 @@ namespace IPA.Config.Stores
return true;
}
private static readonly Dictionary<Type, SerializedMemberInfo[]> objectStructureCache = new Dictionary<Type, SerializedMemberInfo[]>();
private static readonly SingleCreationValueCache<Type, SerializedMemberInfo[]> objectStructureCache
= new SingleCreationValueCache<Type, SerializedMemberInfo[]>();
private static IEnumerable<SerializedMemberInfo> ReadObjectMembers(Type type)
{
if (!objectStructureCache.TryGetValue(type, out var structure))
objectStructureCache.Add(type, structure = ReadObjectMembersInternal(type).ToArray());
return structure;
}
=> objectStructureCache.GetOrAdd(type, t => ReadObjectMembersInternal(type).ToArray());
private static IEnumerable<SerializedMemberInfo> ReadObjectMembersInternal(Type type)
{


+ 1
- 0
IPA.Loader/IPA.Loader.csproj View File

@ -149,6 +149,7 @@
<Compile Include="JsonConverters\SemverRangeConverter.cs" />
<Compile Include="JsonConverters\SemverVersionConverter.cs" />
<Compile Include="Utilities\Async\Coroutines.cs" />
<Compile Include="Utilities\Async\SingleCreationValueCache.cs" />
<Compile Include="Utilities\Async\SingleThreadTaskScheduler.cs" />
<Compile Include="Utilities\Accessor.cs" />
<Compile Include="Utilities\Async\UnityMainThreadTaskScheduler.cs" />


+ 2
- 0
IPA.Loader/Loader/Features/Feature.cs View File

@ -88,6 +88,8 @@ namespace IPA.Loader.Features
/// <value><see langword="true"/> if this <see cref="Feature"/> will be stored on the plugin metadata, <see langword="false"/> otherwise</value>
protected internal virtual bool StoreOnPlugin => true;
// TODO: rework features to take arguments as JSON objects
static Feature()
{
Reset();


+ 12
- 12
IPA.Loader/Loader/PluginComponent.cs View File

@ -1,8 +1,8 @@
using IPA.Config;
using IPA.Loader.Composite;
using IPA.Utilities;
using IPA.Utilities.Async;
using System.Diagnostics.CodeAnalysis;
using IPA.Utilities;
using IPA.Utilities.Async;
using System.Diagnostics.CodeAnalysis;
using UnityEngine;
using UnityEngine.SceneManagement;
// ReSharper disable UnusedMember.Local
@ -28,7 +28,7 @@ namespace IPA.Loader
if (!initialized)
{
UnityGame.SetMainThread();
UnityGame.SetMainThread();
UnityGame.EnsureRuntimeGameVersion();
PluginManager.Load();
@ -47,16 +47,16 @@ namespace IPA.Loader
SceneManager.activeSceneChanged += OnActiveSceneChanged;
SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded;
var unitySched = UnityMainThreadTaskScheduler.Default as UnityMainThreadTaskScheduler;
if (!unitySched.IsRunning)
SceneManager.sceneUnloaded += OnSceneUnloaded;
var unitySched = UnityMainThreadTaskScheduler.Default as UnityMainThreadTaskScheduler;
if (!unitySched.IsRunning)
StartCoroutine(unitySched.Coroutine());
initialized = true;
#if DEBUG
Config.Stores.GeneratedStoreImpl.DebugSaveAssembly("GeneratedAssembly.dll");
initialized = true;
#if DEBUG
Config.Stores.GeneratedStoreImpl.DebugSaveAssembly("GeneratedAssembly.dll");
#endif
}
}


+ 15
- 23
IPA.Loader/Utilities/Accessor.cs View File

@ -1,8 +1,11 @@
using System;
using IPA.Utilities.Async;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
#if NET3
using Net3_Proxy;
using Array = Net3_Proxy.Array;
@ -25,9 +28,6 @@ namespace IPA.Utilities
/// <returns>a reference to the field's value</returns>
public delegate ref U Accessor(ref T obj);
// field name -> accessor
private static readonly Dictionary<string, Accessor> accessors = new Dictionary<string, Accessor>();
private static Accessor MakeAccessor(string fieldName)
{
var field = typeof(T).GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
@ -51,6 +51,9 @@ namespace IPA.Utilities
return (Accessor)dyn.CreateDelegate(typeof(Accessor));
}
// field name -> accessor
private static readonly SingleCreationValueCache<string, Accessor> accessors = new SingleCreationValueCache<string, Accessor>();
/// <summary>
/// Gets an <see cref="Accessor"/> for the field named <paramref name="name"/> on <typeparamref name="T"/>.
/// </summary>
@ -58,11 +61,7 @@ namespace IPA.Utilities
/// <returns>an accessor for the field</returns>
/// <exception cref="MissingFieldException">if the field does not exist on <typeparamref name="T"/></exception>
public static Accessor GetAccessor(string name)
{
if (!accessors.TryGetValue(name, out var accessor))
accessors.Add(name, accessor = MakeAccessor(name));
return accessor;
}
=> accessors.GetOrAdd(name, MakeAccessor);
/// <summary>
/// Accesses a field for an object by name.
@ -149,8 +148,6 @@ namespace IPA.Utilities
/// <param name="val">the new property value</param>
public delegate void Setter(ref T obj, U val);
private static readonly Dictionary<string, (Getter get, Setter set)> props = new Dictionary<string, (Getter get, Setter set)>();
private static (Getter, Setter) MakeAccessors(string propName)
{
var prop = typeof(T).GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
@ -201,12 +198,11 @@ namespace IPA.Utilities
return (getter, setter);
}
private static readonly SingleCreationValueCache<string, (Getter get, Setter set)> props
= new SingleCreationValueCache<string, (Getter get, Setter set)>();
private static (Getter get, Setter set) GetAccessors(string propName)
{
if (!props.TryGetValue(propName, out var access))
props.Add(propName, access = MakeAccessors(propName));
return access;
}
=> props.GetOrAdd(propName, MakeAccessors);
/// <summary>
/// Gets a <see cref="Getter"/> for the property identified by <paramref name="name"/>.
@ -289,8 +285,6 @@ namespace IPA.Utilities
/// <typeparam name="TDelegate">the delegate type to create, and to use as a signature to search for</typeparam>
public static class MethodAccessor<T, TDelegate> where TDelegate : Delegate
{
private static readonly Dictionary<string, TDelegate> methods = new Dictionary<string, TDelegate>();
static MethodAccessor()
{
// ensure that first argument of delegate type is valid
@ -325,6 +319,8 @@ namespace IPA.Utilities
return (TDelegate)Delegate.CreateDelegate(AccessorDelegateInfo<TDelegate>.Type, method, true);
}
private static readonly SingleCreationValueCache<string, TDelegate> methods = new SingleCreationValueCache<string, TDelegate>();
/// <summary>
/// Gets a delegate to the named method with the signature specified by <typeparamref name="TDelegate"/>.
/// </summary>
@ -333,11 +329,7 @@ namespace IPA.Utilities
/// <exception cref="MissingMethodException">if <paramref name="name"/> does not represent the name of a method with the given signature</exception>
/// <exception cref="ArgumentException">if the method found returns a type incompatable with the return type of <typeparamref name="TDelegate"/></exception>
public static TDelegate GetDelegate(string name)
{
if (!methods.TryGetValue(name, out var del))
methods.Add(name, del = MakeDelegate(name));
return del;
}
=> methods.GetOrAdd(name, MakeDelegate);
}
}

+ 154
- 0
IPA.Loader/Utilities/Async/SingleCreationValueCache.cs View File

@ -0,0 +1,154 @@
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
var val = creator(key);
while (!dict.TryUpdate(key, (null, val), cmp))
throw new InvalidOperationException();
wh.Set();
return val;
}
}
}
}

Loading…
Cancel
Save