-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
Currently, there doesn't seem to be a way to implement a lock-free caching algorithm using a ConditionalWeakTable<TKey, TValue> without having to allocate a delegate + closure to insert a value into the table. That is, consider this:
// Try to get an existing value for the key
if (table.TryGetValue(key, out Foo? result))
{
return result;
}
// Value does not exist, create it (this is potentially expensive)
result = CreateValueForKey(key, configuration);
// Add the value into the table in a way that makes it so that if we're racing against
// another thread, we're guaranteeing that all threads will still only see and retrieve
// the same instance from the table.
table.GetValue(key, _ => result);That last step would ideally be some table.GetOrAdd(key, result) call, with no delegate needed.
I know you can also do TryAdd and GetValue again if false, but that does one extra unnecessary lookup.
Also consider this similar example:
table.GetValue(key, key => CreateValueForKey(key, configuration));This works, but it captures configuration and allocates a display and non-cached delegate unnecessarily.
Looking at ConditionalWeakTable<TKey, TValue>, it seems easy enough to add these new APIs:
// Gets the current value, or adds the new one and returns it
public TValue GetOrAdd(TKey key, TValue value);
// Overload of 'GetValue' also taking additional state
public TValue GetValue<TState>(TKey key, CreateValueCallback<TState> createValueCallback, TState state);These two convenience methods can be used where appropriate. For instance, it might be simpler in some cases to use the new GetValue overload, whereas in other cases where you might want more control, you might manually try the first lookup, then do some additional work if that fails, and finally call GetOrAdd with the new value you produced.
API Proposal
namespace System.Runtime.CompilerServices;
public sealed class ConditionalWeakTable<TKey, TValue>
{
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
public void Add(TKey key, TValue value);
public bool TryAdd(TKey key, TValue value);
public void AddOrUpdate(TKey key, TValue value);
+ public TValue GetOrAdd(TKey key, TValue value);
public bool Remove(TKey key);
public void Clear();
public TValue GetValue(TKey key, CreateValueCallback createValueCallback);
+ public TValue GetValue<TState>(TKey key, CreateValueCallback<TState> createValueCallback, TState state);
+ where TState : allows ref struct
public TValue GetOrCreateValue(TKey key);
public delegate TValue CreateValueCallback(TKey key);
+ public delegate TValue CreateValueCallback<TState>(TKey key, TState state)
+ where TState : allows ref struct
}API Usage
Updating the example above to use the new API:
if (table.TryGetValue(key, out Foo? result))
{
return result;
}
result = CreateValueForKey(key, state);
return table.GetOrAdd(key, result);AND
table.GetValue(key, CreateValueForKey, state);Alternative Designs
Keep using GetValue and waste the delegate + closure allocation (also it's much clunkier).
Risks
None that I can see, it's just two small new APIs with no additional changes needed.
The functionality is the same as already available today, just more effcient.