247 lines
7.7 KiB
C#
247 lines
7.7 KiB
C#
//-----------------------------------------------------------------------
|
|
// <copyright file="Cache.cs" company="Sirenix IVS">
|
|
// Copyright (c) 2018 Sirenix IVS
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// </copyright>
|
|
//-----------------------------------------------------------------------
|
|
|
|
namespace VRC.Udon.Serialization.OdinSerializer.Utilities
|
|
{
|
|
using System;
|
|
using System.Threading;
|
|
|
|
/// <summary>
|
|
/// Provides an easy way of claiming and freeing cached values of any non-abstract reference type with a public parameterless constructor.
|
|
/// <para />
|
|
/// Cached types which implement the <see cref="ICacheNotificationReceiver"/> interface will receive notifications when they are claimed and freed.
|
|
/// <para />
|
|
/// Only one thread should be holding a given cache instance at a time if <see cref="ICacheNotificationReceiver"/> is implemented, since the invocation of
|
|
/// <see cref="ICacheNotificationReceiver.OnFreed()"/> is not thread safe, IE, weird stuff might happen if multiple different threads are trying to free
|
|
/// the same cache instance at the same time. This will practically never happen unless you're doing really strange stuff, but the case is documented here.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type which is cached.</typeparam>
|
|
/// <seealso cref="System.IDisposable" />
|
|
public sealed class Cache<T> : IDisposable where T : class, new()
|
|
{
|
|
private static readonly bool IsNotificationReceiver = typeof(ICacheNotificationReceiver).IsAssignableFrom(typeof(T));
|
|
private static object[] FreeValues = new object[4];
|
|
|
|
private bool isFree;
|
|
|
|
private static volatile int THREAD_LOCK_TOKEN = 0;
|
|
|
|
private static int maxCacheSize = 5;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the maximum size of the cache. This value can never go beneath 1.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The maximum size of the cache.
|
|
/// </value>
|
|
public static int MaxCacheSize
|
|
{
|
|
get
|
|
{
|
|
return Cache<T>.maxCacheSize;
|
|
}
|
|
|
|
set
|
|
{
|
|
Cache<T>.maxCacheSize = Math.Max(1, value);
|
|
}
|
|
}
|
|
|
|
private Cache()
|
|
{
|
|
this.Value = new T();
|
|
this.isFree = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cached value.
|
|
/// </summary>
|
|
public T Value;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this cached value is free.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if this cached value is free; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool IsFree { get { return this.isFree; } }
|
|
|
|
/// <summary>
|
|
/// Claims a cached value of type <see cref="T"/>.
|
|
/// </summary>
|
|
/// <returns>A cached value of type <see cref="T"/>.</returns>
|
|
public static Cache<T> Claim()
|
|
{
|
|
Cache<T> result = null;
|
|
|
|
// Very, very simple spinlock implementation
|
|
// this lock will almost never be contested
|
|
// and it will never be held for more than
|
|
// an instant; therefore, we want to avoid paying
|
|
// the lock(object) statement's semaphore
|
|
// overhead.
|
|
while (true)
|
|
{
|
|
if (Interlocked.CompareExchange(ref THREAD_LOCK_TOKEN, 1, 0) == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We now hold the lock
|
|
var freeValues = FreeValues;
|
|
var length = freeValues.Length;
|
|
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
result = (Cache<T>)freeValues[i];
|
|
if (!object.ReferenceEquals(result, null))
|
|
{
|
|
freeValues[i] = null;
|
|
result.isFree = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Release the lock
|
|
THREAD_LOCK_TOKEN = 0;
|
|
|
|
if (result == null)
|
|
{
|
|
result = new Cache<T>();
|
|
}
|
|
|
|
if (IsNotificationReceiver)
|
|
{
|
|
(result.Value as ICacheNotificationReceiver).OnClaimed();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases a cached value.
|
|
/// </summary>
|
|
/// <param name="cache">The cached value to release.</param>
|
|
/// <exception cref="System.ArgumentNullException">The cached value to release is null.</exception>
|
|
public static void Release(Cache<T> cache)
|
|
{
|
|
if (cache == null)
|
|
{
|
|
throw new ArgumentNullException("cache");
|
|
}
|
|
|
|
if (cache.isFree) return;
|
|
|
|
// No need to call this method inside the lock, which might do heavy work
|
|
// there is a thread safety hole here, actually - if several different threads
|
|
// are trying to free the same cache instance, OnFreed might be called several
|
|
// times concurrently for the same cached value.
|
|
if (IsNotificationReceiver)
|
|
{
|
|
(cache.Value as ICacheNotificationReceiver).OnFreed();
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
if (Interlocked.CompareExchange(ref THREAD_LOCK_TOKEN, 1, 0) == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We now hold the lock
|
|
|
|
if (cache.isFree)
|
|
{
|
|
// Release the lock and leave - job's done already
|
|
THREAD_LOCK_TOKEN = 0;
|
|
return;
|
|
}
|
|
|
|
|
|
cache.isFree = true;
|
|
|
|
var freeValues = FreeValues;
|
|
var length = freeValues.Length;
|
|
|
|
bool added = false;
|
|
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
if (object.ReferenceEquals(freeValues[i], null))
|
|
{
|
|
freeValues[i] = cache;
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!added && length < MaxCacheSize)
|
|
{
|
|
var newArr = new object[length * 2];
|
|
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
newArr[i] = freeValues[i];
|
|
}
|
|
|
|
newArr[length] = cache;
|
|
|
|
FreeValues = newArr;
|
|
}
|
|
|
|
// Release the lock
|
|
THREAD_LOCK_TOKEN = 0;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an implicit conversion from <see cref="Cache{T}"/> to <see cref="T"/>.
|
|
/// </summary>
|
|
/// <param name="cache">The cache to convert.</param>
|
|
/// <returns>
|
|
/// The result of the conversion.
|
|
/// </returns>
|
|
public static implicit operator T(Cache<T> cache)
|
|
{
|
|
if (cache == null)
|
|
{
|
|
return default(T);
|
|
}
|
|
|
|
return cache.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases this cached value.
|
|
/// </summary>
|
|
public void Release()
|
|
{
|
|
Release(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases this cached value.
|
|
/// </summary>
|
|
void IDisposable.Dispose()
|
|
{
|
|
Cache<T>.Release(this);
|
|
}
|
|
}
|
|
} |