在使用C#中的缓存时,哪个是哪个选择?
我对编译器级别感兴趣,其中哪一个是最优雅/性能的解决方案。
例如,.NET编译器是否会使用任何技巧来知道何时代码同步运行并避免创建/运行不必要的异步代码?
选项1,使用async/await
并使用Task.FromResult
进行缓存值;
public async Task<T> GetValue<T>(string key)
{
if (_cache.containsKey(key))
{
// 99% of the time will hit this
return Task.FromResult(_cache.GetItem(key));
}
return await _api.GetValue(key);
}
选项2,避免使用async/await
,并使用GetAwaiter().GetResult()
之类的东西,几次将点击API端点。
public T GetValue<T>(string key)
{
if (_cache.containsKey(key))
{
// 99% of the time will hit this
return _cache.GetItem(key);
}
return _api.GetValue(key).GetAwaiter().GetResult();
}
任何见解都将不胜感激。
官方方法是缓存Task<T>
,而不是T
。
这也具有一个优势,如果有人请求该值,则可以启动请求以获取值,然后缓存结果,即过程中的Task<T>
。如果其他人在请求完成之前请求缓存的值,则还给出了相同的过程中Task<T>
,并且您最终不会提出两个请求。
例如:
public Task<T> GetValue<T>(string key)
{
// Prefer a TryGet pattern if you can, to halve the number of lookups
if (_cache.containsKey(key))
{
return _cache.GetItem(key);
}
var task = _api.GetValue(key);
_cache.Add(key, task);
return task;
}
请注意,在这种情况下,您需要考虑失败:如果对API的请求失败,那么您将缓存一个包含异常的Task
。这可能是您想要的,但可能不是。
如果由于某种原因无法执行此操作,则官方建议是将ValueTask<T>
用于高性能方案。这种类型有一些陷阱(例如,您不能等待两次),因此我建议阅读此内容。如果您没有高性能要求,则Task.FromResult
很好。
您的第一个无法正常工作。最简单的是,大多数时候要去的是:
public async Task<T> GetValueAsync<T>(string key)
{
if (_cache.ContainsKey(key))
{
return _cache.GetItem(key);
}
T result = await _api.GetValueAysnc(key);
_cache.Add(key, result);
return result;
}
或更好的话,如果可能的话:
public async Task<T> GetValueAsync<T>(string key)
{
if (_cache.TryGet(key, out T result))
{
return result;
}
result = await _api.GetValueAysnc(key);
_cache.Add(key, result);
return result;
}
这可以正常工作,并在值在缓存中时返回已经完成的任务,因此await
将立即继续。
但是,如果值在缓存中大部分时间和该方法通常被称为足以使async
周围的额外设备可供使用,以使您避免它完全在这种情况下:
public Task<T> GetValueAsync<T>(string key)
{
if (_cache.TryGet(key, out Task<T> result))
{
return result;
}
return GetAndCacheValueAsync(string key);
}
private async Task<T> GetAndCacheValueAsync<T>(string key)
{
var task = _api.GetValueAysnc(key);
result = await task;
_cache.Add(key, task);
return result;
}
在这里,如果该值被缓存,我们避免了async
周围的状态机器,也避免创建新的Task<T>
,因为我们存储了一个实际的Task
。这些仅在第一种情况下完成。
您要寻找的可能是回忆。
实现可能是这样的:
public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> f)
{
var cache = new ConcurrentDictionary<T, TResult>();
return a => cache.GetOrAdd(a, f);
}
Measure(() => slowSquare(2)); // 00:00:00.1009680
Measure(() => slowSquare(2)); // 00:00:00.1006473
Measure(() => slowSquare(2)); // 00:00:00.1006373
var memoizedSlow = slowSquare.Memoize();
Measure(() => memoizedSlow(2)); // 00:00:00.1070149
Measure(() => memoizedSlow(2)); // 00:00:00.0005227
Measure(() => memoizedSlow(2)); // 00:00:00.0004159
源
首先,这需要链接速度rant:
https://ericlippert.com/2012/12/17/performance-rant/
这样的微观量通常留给JIT。我的经验法则是,如果您确实需要这种差异,那么您就可以处理实时编程。而且,对于实时进行垃圾收集的运行时,例如.NET可能是错误的环境。具有直接内存管理(例如不安全的代码) - 甚至本机C 或汇编器的内容都会更好。
其次,任务可能只是错误的工具。也许您实际想要的是懒惰[T]?还是5种不同的Chache类中的任何一个?(与计时器一样,特定用户界面技术大约有一个)。
可以将任何工具用于许多目的。但是任务是用于多任务处理,并且有更好的工具用于缓存和懒惰的初始化。懒惰[t]甚至本质上是线程保存。