-
Notifications
You must be signed in to change notification settings - Fork 39
Atomic value creation #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| // if value !created, no dispose, cannot create - throws object disposed exception | ||
| // if created, dispose value | ||
| [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] | ||
| public class DisposableAtomic<K, V> : IDisposable where V : IDisposable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could also create a ScopedLazy, allowing the user to substitute in Lazy behavior if preferred to Atomic.
This would work the same way - check the value is created property when disposing scope.
| namespace BitFaster.Caching | ||
| { | ||
| [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] | ||
| public class AsyncAtomic<K, V> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would support both sync and async.
Sync adds are slightly penalized vs pure Lazy because Initializer.CreateValue creates a TCS, which internally creates a Task. These are both pretty lightweight - likely cheap compared to value factory.
namespace BitFaster.Caching
{
[DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")]
public class AsyncAtomic<K, V>
{
private Initializer initializer;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private V value;
public AsyncAtomic()
{
this.initializer = new Initializer();
}
public AsyncAtomic(V value)
{
this.value = value;
}
public V GetValue(K key, Func<K, V> valueFactory)
{
if (this.initializer == null)
{
return this.value;
}
return CreateValue(key, valueFactory);
}
public async Task<V> GetValueAsync(K key, Func<K, Task<V>> valueFactory)
{
if (this.initializer == null)
{
return this.value;
}
return await CreateValueAsync(key, valueFactory).ConfigureAwait(false);
}
public bool IsValueCreated => this.initializer == null;
public V ValueIfCreated
{
get
{
if (!this.IsValueCreated)
{
return default;
}
return this.value;
}
}
private V CreateValue(K key, Func<K, V> valueFactory)
{
Initializer init = this.initializer;
if (init != null)
{
this.value = init.CreateValue(key, valueFactory);
this.initializer = null;
}
return this.value;
}
private async Task<V> CreateValueAsync(K key, Func<K, Task<V>> valueFactory)
{
Initializer init = this.initializer;
if (init != null)
{
this.value = await init.CreateValueAsync(key, valueFactory).ConfigureAwait(false);
this.initializer = null;
}
return this.value;
}
private class Initializer
{
private object syncLock = new object();
private bool isInitialized;
private Task<V> valueTask;
public V CreateValue(K key, Func<K, V> valueFactory)
{
var tcs = new TaskCompletionSource<V>(TaskCreationOptions.RunContinuationsAsynchronously);
var synchronizedTask = Synchronized.Initialize(ref this.valueTask, ref isInitialized, ref syncLock, tcs.Task);
if (ReferenceEquals(synchronizedTask, tcs.Task))
{
try
{
var value = valueFactory(key);
tcs.SetResult(value);
return value;
}
catch (Exception ex)
{
Volatile.Write(ref isInitialized, false);
tcs.SetException(ex);
throw;
}
}
// TODO: how dangerous is this?
// it can block forever if value factory blocks
return synchronizedTask.GetAwaiter().GetResult();
}
public async Task<V> CreateValueAsync(K key, Func<K, Task<V>> valueFactory)
{
var tcs = new TaskCompletionSource<V>(TaskCreationOptions.RunContinuationsAsynchronously);
var synchronizedTask = Synchronized.Initialize(ref this.valueTask, ref isInitialized, ref syncLock, tcs.Task);
if (ReferenceEquals(synchronizedTask, tcs.Task))
{
try
{
var value = await valueFactory(key).ConfigureAwait(false);
tcs.SetResult(value);
return value;
}
catch (Exception ex)
{
Volatile.Write(ref isInitialized, false);
tcs.SetException(ex);
throw;
}
}
return await synchronizedTask.ConfigureAwait(false);
}
}
}
internal static class Synchronized
{
public static V Initialize<V>(ref V target, ref bool initialized, ref object syncLock, V value)
{
// Fast path
if (Volatile.Read(ref initialized))
{
return target;
}
lock (syncLock)
{
if (!Volatile.Read(ref initialized))
{
target = value;
Volatile.Write(ref initialized, true);
}
}
return target;
}
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then use a decorator:
namespace BitFaster.Caching
{
public class AtomicCacheDecorator<K, V> : ICache<K, V>
{
private readonly ICache<K, AsyncAtomic<K, V>> cache;
public AtomicCacheDecorator(ICache<K, AsyncAtomic<K, V>> cache)
{
this.cache = cache;
}
public int Capacity => this.cache.Capacity;
public int Count => this.cache.Count;
public void AddOrUpdate(K key, V value)
{
cache.AddOrUpdate(key, new AsyncAtomic<K, V>(value));
}
public void Clear()
{
this.cache.Clear();
}
public V GetOrAdd(K key, Func<K, V> valueFactory)
{
var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic<K, V>());
return synchronized.GetValue(key, valueFactory);
}
public Task<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
{
var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic<K, V>());
return synchronized.GetValueAsync(key, valueFactory);
}
public void Trim(int itemCount)
{
this.cache.Trim(itemCount);
}
public bool TryGet(K key, out V value)
{
AsyncAtomic<K, V> output;
bool ret = cache.TryGet(key, out output);
if (ret && output.IsValueCreated)
{
value = output.ValueIfCreated;
return true;
}
value = default;
return false;
}
public bool TryRemove(K key)
{
return this.cache.TryRemove(key);
}
public bool TryUpdate(K key, V value)
{
return cache.TryUpdate(key, new AsyncAtomic<K, V>(value)); ;
}
}
}
By caching Lazy, we can easily guarantee a single invocation of the value create logic. However, the standard Lazy implementation has some drawbacks:
This PR introduces a family of Atomic classes that guarantee atomic value creation, do not cache exceptions, handle IDisposable, async value creation and async creation of IDisposable values.