Created
July 9, 2020 22:36
-
-
Save aarondandy/45782fa9475ad4d70dd357f84c3efff5 to your computer and use it in GitHub Desktop.
async blocking memory cache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Extensions.Caching.Memory; | |
using System; | |
using System.Collections.Concurrent; | |
using System.Linq; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace CacheTesting | |
{ | |
class Program | |
{ | |
static async Task Main(string[] args) | |
{ | |
var cache = new MemoryCache(new MemoryCacheOptions()); | |
var asyncCache = new AsyncMemoryCache(cache); | |
var values = await Task.WhenAll(Enumerable.Range(0, 128).Select(async n => | |
{ | |
var key = (n / 10).ToString(); | |
var delay = 100 + (n % 10); | |
// bad things | |
//return await cache.GetOrCreateAsync(key, async e => | |
//{ | |
// // pretend this is an in memory thing that is expensive to construct | |
// await Task.Delay(delay).ConfigureAwait(false); | |
// return RandomNext(100); | |
//}).ConfigureAwait(false); | |
// good things | |
return await asyncCache.GetOrCreateBlockingAsync(key, async e => | |
{ | |
// pretend this is an in memory thing that is expensive to construct | |
await Task.Delay(delay).ConfigureAwait(false); | |
return RandomNext(100); | |
}).ConfigureAwait(false); | |
})).ConfigureAwait(false); | |
for (var i = 0; i < values.Length; i++) | |
{ | |
Console.WriteLine($"{i}:\t{values[i]}"); | |
} | |
Console.ReadKey(); | |
} | |
static Random rand = new Random(); | |
static int RandomNext(int n) | |
{ | |
lock (rand) | |
{ | |
return rand.Next(n); | |
} | |
} | |
} | |
// Idea from https://michaelscodingspot.com/cache-implementations-in-csharp-net/ | |
class AsyncMemoryCache : IMemoryCache | |
{ | |
public AsyncMemoryCache(IMemoryCache memoryCache) | |
{ | |
_cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); | |
_locks = new ConcurrentDictionary<object, SemaphoreSlim>(); | |
} | |
private readonly IMemoryCache _cache; | |
private readonly ConcurrentDictionary<object, SemaphoreSlim> _locks; | |
public Task<TItem> GetOrCreateBlockingAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory, CancellationToken cancellationToken = default) => | |
_cache.TryGetValue(key, out TItem result) | |
? Task.FromResult(result) | |
: GetOrCreateLocked(key, (e, _) => factory(e), cancellationToken); | |
public Task<TItem> GetOrCreateBlockingAsync<TItem>(object key, Func<ICacheEntry, CancellationToken, Task<TItem>> factory, CancellationToken cancellationToken = default) => | |
_cache.TryGetValue(key, out TItem result) | |
? Task.FromResult(result) | |
: GetOrCreateLocked(key, factory, cancellationToken); | |
private async Task<TItem> GetOrCreateLocked<TItem>(object key, Func<ICacheEntry, CancellationToken, Task<TItem>> factory, CancellationToken cancellationToken) | |
{ | |
TItem result; | |
var semaphore = _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); | |
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); | |
try | |
{ | |
if (!_cache.TryGetValue(key, out result)) | |
{ | |
result = await factory(_cache.CreateEntry(key), cancellationToken).ConfigureAwait(false); | |
_cache.Set(key, result); | |
} | |
} | |
finally | |
{ | |
_locks.TryRemove(key, out _); | |
semaphore.Release(); | |
// okay but who will dispose of the semaphore? | |
} | |
return result; | |
} | |
public bool TryGetValue(object key, out object value) | |
{ | |
return _cache.TryGetValue(key, out value); | |
} | |
public ICacheEntry CreateEntry(object key) | |
{ | |
return _cache.CreateEntry(key); | |
} | |
public void Remove(object key) | |
{ | |
_cache.Remove(key); | |
} | |
public void Dispose() | |
{ | |
_cache.Dispose(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment