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

7
我正在尝试将下面的方法(简化示例)转换为异步方法,因为cacheMissResolver调用可能会花费很长时间(数据库查找、网络调用):
```html

我正在尝试将下面的方法(简化示例)转换为异步方法,因为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在异步方法中替换锁语句是正确的吗?(我无法在锁语句的主体中使用await。) cacheMissResolver参数应该改为Func<Task<Thing>>类型吗?虽然这会让调用者承担确保解析程序函数是异步的负担(包装在Task.Run中),但如果需要很长时间,它将被卸载到后台线程。
谢谢。

1
考虑使用 AsyncLock - Timothy Shields
@Timothy Shields - 看起来很有用!谢谢。 - rob
1
@dbc - 在真正的代码中,缓存是InMemoryCache的一个实例;但它也可以是任何合适的结构。我不想让这个例子变得复杂。 - rob
1
@rob 尽管如此,它非常相关,因为您在同时从多个线程访问数据结构,而大多数数据结构并没有为此而构建。需要一个专门设计支持此功能的数据结构,例如ConcurrentDictionary - Servy
1
@rob 抱歉,我把这个问题和另一个问题搞混了。 - Servy
显示剩余2条评论
2个回答

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);
    }
  }
}

如果您有另一种方法定期清理缓存,那么您可以决定此额外检查是不必要的。

谢谢你。我没有考虑过缓存任务本身。感谢你在这个主题上写了这么多,我发现你的一些文章对学习更多知识很有用! - rob

3

在异步方法中使用SemaphoreSlim来替换锁语句是正确的方法吗?

是的。

我应该将cacheMissResolver参数的类型更改为Func<Task<Thing>>吗?

是的。这将允许调用者提供一个本质上是异步操作(如IO)而不仅仅适用于长时间运行的CPU绑定工作。(如果调用者想要支持CPU绑定工作,他们可以简单地自己使用Task.Run。)


除此之外,只需注意没有必要使用await Task.FromResult(...);。将值包装在Task中,然后立即解包它是没有意义的。在这种情况下,直接返回缓存的值即可。你所做的并没有真正的错误,只是使代码变得复杂和混乱。


谢谢。我没有意识到在 Task.FromResult(...) 上使用 await 的影响。 - rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接