避免异步/等待,如果我知道大多数情况下的结果将被缓存



在使用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]甚至本质上是线程保存。

相关内容

最新更新