Thread synchronisation with SemaphoreSlim and async delegate calls
Although running a parallel foreach
that makes async
calls is a pretty usual task, I found my self in a complicated situation, when async
delegate
calls got in the way!
The situation arose while I was trying to write a MemoryCache manager for my application (the manager was actually a wrapper of MemoryCache Class).
The following gist file, is a basic structure of what I was trying to do: Ask MemoryCacheManager
for an item with AddOrGetExisting
by passing a key
to find the object and a generic delegate Func task
that actually returns the object.
public class SomeClass{
private readonly ICacheManager _cacheManager;
public void DoSomething(){
Parallel.ForEach(list, async item =>{
//...
var item = await _cacheManager.AddOrGetExisting(item.Id, GetAllData);
//...
});
}
public async List<object> GetAllData(){
//...
return await GetDataFromDb();
}
}
public class MemoryCacheManager : ICacheManager {
//...
public async Task AddOrGetExisting(string key, Func task)
{
item = _cache.Get(key);
if (item == null)
{
item = await task();
_cache.Set(key, item, DateTime.Now.AddSeconds(SomeExpiration));
}
return (T)item;
}
}
Since this is not thread safe, my first approach was to use the lock statement, and as a countermeasure for not being able to have awaitable calls inside a lock, I played around with Task.GetAwaiter().GetResult()
. Ugly :/
Sychronous block on an asycrhonous task (that could be intensive) is never a good idea.
As Stephen Cleary states in his blog:
The code … will synchronously block until the task completes. As such, it is subject to the same old deadlock problems as Wait and Result…
…
“GetResult” actually means “check the task for errors”…
Finally, I found a better approach after reading a few things about thread sycrhonization. I end up with the following solution that uses a semaphore slim:
public class SomeClass{
...
private readonly ICacheManager _cacheManager;
...
public void DoSomething(){
Parallel.ForEach(list, async item =>{
...
var cachedItem = await _cacheManager.AddOrGetExisting(item.Id, () =>{return Task.FromResult(item)});
...
});
}
}
public class MemoryCacheManager : ICacheManager {
//...
private static SemaphoreSlim slim = new SemaphoreSlim(1, 1);
//...
public async Task AddOrGetExisting(string key, Func task)
{
var item = _cache.Get(key);
if (item == null)
{
await slim.WaitAsync();
try
{
item = _cache.Get(key);
if (item == null)
{
item = await task();
_cache.Set(key, item, DateTime.Now.AddSeconds(SomeExpiration));
}
}
finally
{
slim.Release();
}
}
return (T)item;
}
}
* As you can see, there are two _cache.Get(key)
, a pattern that is called double-check locking. In short, locks are expensive and there is no reason to get one, if the requested object exists in the cache. Read more about that here and here