在C#中将方法转换为异步的正确方法?

在C#中将方法转换为异步的正确方法?,c#,asynchronous,async-await,C#,Asynchronous,Async Await,我正在尝试将以下方法(简化示例)转换为异步方法,因为cachemissrolver调用在时间上可能很昂贵(数据库查找、网络调用): //同步版本 公共类ThingCache { 私有静态只读对象_lockObj; //…其他东西 公共对象获取(字符串键,Func cachemissrolver) { if(cache.Contains(key)) 返回缓存[键]; 物项; 锁 { if(cache.Contains(key)) 返回缓存[键]; item=cacheMissResolver();

我正在尝试将以下方法(简化示例)转换为异步方法,因为
cachemissrolver
调用在时间上可能很昂贵(数据库查找、网络调用):

//同步版本
公共类ThingCache
{
私有静态只读对象_lockObj;
//…其他东西
公共对象获取(字符串键,Func cachemissrolver)
{
if(cache.Contains(key))
返回缓存[键];
物项;
锁
{
if(cache.Contains(key))
返回缓存[键];
item=cacheMissResolver();
cache.Add(key,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;
    }
}
//异步尝试
公共类ThingCache
{
私有静态只读信号量lim _lockObj=新信号量lim(1);
//…其他东西
//尝试#1
公共异步任务Get(字符串键,Func cachemissrolver)
{
if(cache.Contains(key))
返回wait Task.FromResult(缓存[key]);
物项;
wait_lockObj.WaitAsync();
尝试
{
if(cache.Contains(key))
返回wait Task.FromResult(缓存[key]);
item=等待任务。运行(cacheMissResolver)。配置等待(false);
_cache.Add(key,item);
}
最后
{
_lockObj.Release();
}
退货项目;
}
//尝试#2
公共异步任务Get(字符串键,Func cachemissrolver)
{
if(cache.Contains(key))
返回wait Task.FromResult(缓存[key]);
物项;
wait_lockObj.WaitAsync();
尝试
{
if(cache.Contains(key))
返回wait Task.FromResult(缓存[key]);
item=await cacheMissResolver().ConfigureAwait(false);
_cache.Add(key,item);
}
最后
{
_lockObj.Release();
}
退货项目;
}
}
使用
SemaphoreSlim
是在异步方法中替换lock语句的正确方法吗?(我不能在lock语句的正文中等待。)

我应该改为使用
Func
类型的
cacheMissResolver
参数吗?尽管这会给调用者带来确保解析器func异步的负担(包装在
Task.Run
中),但我知道如果需要很长时间,它将被卸载到后台线程中

谢谢

在异步方法中使用SemaphoreSlim替换lock语句是否正确

我应该改为使用
Func
类型的
cacheMissResolver
参数吗

对。它将允许调用者提供固有的异步操作(如IO),而不是使其仅适用于长时间运行的CPU受限工作。(尽管仍然支持CPU受限的工作,但只要让调用方使用
任务即可。如果他们想这样做,就自己运行
。)



除此之外,请注意让
等待Task.FromResult(…)没有意义
任务中包装一个值
只是立即将其展开是没有意义的。在这种情况下直接使用结果,在这种情况下,直接返回缓存的值。你所做的并不是真的错了,它只是不必要的复杂化/混淆代码。

< P>如果你的缓存在内存中(看起来是这样),那么考虑缓存任务而不是结果。如果两个方法请求相同的密钥,则只会发出一个解析请求,这有一个很好的side属性。此外,由于只有缓存被锁定(而不是解析操作),因此可以继续使用简单的锁定

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);
    }
  }
}
公共类ThingCache
{
私有静态只读对象_lockObj;
公共异步任务GetAsync(字符串键,Func cacheMissResolver)
{
锁
{
if(cache.Contains(key))
返回缓存[键];
var task=cachemissrolver();
_cache.Add(键、任务);
}
}
}
但是,这也会缓存您可能不希望的异常。避免这种情况的一种方法是允许异常任务最初进入缓存,但在发出下一个请求时将其删除:

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);
    }
  }
}
公共类ThingCache
{
私有静态只读对象_lockObj;
公共异步任务GetAsync(字符串键,Func cacheMissResolver)
{
锁
{
if(cache.Contains(key))
{
if(缓存[key].Status==TaskStatus.RanToCompletion)
返回缓存[键];
缓存。删除(键);
}
var task=cachemissrolver();
_cache.Add(键、任务);
}
}
}

如果您有另一个进程定期修剪缓存,您可能会认为此额外检查是不必要的。

请考虑使用。@Timothy Shields-这看起来很有用!谢谢。@dbc-在真正的代码缓存中是InMemoryCache的一个实例;但它可以是任何合适的结构。我不想让这个例子复杂化。@rob这是非常相关的,因为您同时从多个线程访问数据结构,而大多数线程都不支持这一点。它需要一个专门设计的数据结构来支持它,比如
ConcurrentDictionary
@rob抱歉,我把这个问题和另一个问题混淆了。谢谢。我还没有领会在任务中使用等待的含义。FromResult(…)。谢谢。我没有想过缓存任务本身。也为写这么多关于这个主题的文章干杯,发现你的一些文章对学习更多有用!
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);
    }
  }
}