我正在尝试将以下方法(简化示例)转换为异步,因为cacheMissResolver
调用在时间方面可能很昂贵(数据库查找,网络调用):
// Synchronous version
public class ThingCache
{
private static readonly object _lockObj;
// ... other stuff
public Thing Get(string key, Func<Thing> cacheMissResolver)
{
if (cache.Contains(key))
return cache[key];
Thing item;
lock(_lockObj)
{
if (cache.Contains(key))
return cache[key];
item = cacheMissResolver();
cache.Add(key, item);
}
return item;
}
}
关于消费异步方法的网上有很多材料,但我在生产它们时发现的建议似乎不那么明确。鉴于这是打算成为图书馆的一部分,我的尝试是否正确?
// Asynchronous attempts
public class ThingCache
{
private static readonly SemaphoreSlim _lockObj = new SemaphoreSlim(1);
// ... other stuff
// attempt #1
public async Task<Thing> Get(string key, Func<Thing> cacheMissResolver)
{
if (cache.Contains(key))
return await Task.FromResult(cache[key]);
Thing item;
await _lockObj.WaitAsync();
try
{
if (cache.Contains(key))
return await Task.FromResult(cache[key]);
item = await Task.Run(cacheMissResolver).ConfigureAwait(false);
_cache.Add(key, item);
}
finally
{
_lockObj.Release();
}
return item;
}
// attempt #2
public async Task<Thing> Get(string key, Func<Task<Thing>> cacheMissResolver)
{
if (cache.Contains(key))
return await Task.FromResult(cache[key]);
Thing item;
await _lockObj.WaitAsync();
try
{
if (cache.Contains(key))
return await Task.FromResult(cache[key]);
item = await cacheMissResolver().ConfigureAwait(false);
_cache.Add(key, item);
}
finally
{
_lockObj.Release();
}
return item;
}
}
使用SemaphoreSlim
正确的方法来替换异步方法中的锁语句吗? (我无法在锁定声明的正文中等待。)
我应该改为cacheMissResolver
类型的Func<Task<Thing>>
参数吗?虽然这会增加确保解析器func在调用者上是异步的负担(包装在Task.Run
中,但我知道如果需要很长时间,它将被卸载到后台线程。)
感谢。
答案 0 :(得分:3)
使用SemaphoreSlim是否是在异步方法中替换lock语句的正确方法?
是
我应该改为
cacheMissResolver
类型的Func<Task<Thing>>
参数吗?
是。它将允许调用者提供固有的异步操作(例如IO),而不是使其仅适用于长时间运行 CPU绑定工作的工作。 (通过简单地让调用者自己使用Task.Run
来支持CPU绑定工作,如果这是他们想要做的事情。)
除此之外,请注意,await Task.FromResult(...);
在Task
中包装值只是为了立即打开它是毫无意义的。只需在这种情况下直接使用结果,在这种情况下,直接返回缓存的值。你正在做的不是错误,它只是不必要地使代码变得复杂/混乱。
答案 1 :(得分:3)
如果您的缓存是内存中(看起来是这样),那么请考虑缓存任务而不是结果。如果两个方法请求相同的密钥,则只有一个解析请求,这有一个很好的边属性。此外,由于只锁定了缓存(而不是解析操作),因此您可以继续使用简单锁定。
public class ThingCache
{
private static readonly object _lockObj;
public async Task<Thing> GetAsync(string key, Func<Task<Thing>> cacheMissResolver)
{
lock (_lockObj)
{
if (cache.Contains(key))
return cache[key];
var task = cacheMissResolver();
_cache.Add(key, task);
}
}
}
但是,这也会缓存您可能不想要的异常。避免这种情况的一种方法是允许异常任务最初进入缓存,但在下一个请求发生时将其删除:
public class ThingCache
{
private static readonly object _lockObj;
public async Task<Thing> GetAsync(string key, Func<Task<Thing>> cacheMissResolver)
{
lock (_lockObj)
{
if (cache.Contains(key))
{
if (cache[key].Status == TaskStatus.RanToCompletion)
return cache[key];
cache.Remove(key);
}
var task = cacheMissResolver();
_cache.Add(key, task);
}
}
}
如果您有另一个进程定期修剪缓存,您可以决定不需要额外检查。