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

时间:2015-06-03 20:07:42

标签: c# asynchronous async-await

我正在尝试将以下方法(简化示例)转换为异步,因为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中,但我知道如果需要很长时间,它将被卸载到后台线程。)

感谢。

2 个答案:

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

如果您有另一个进程定期修剪缓存,您可以决定不需要额外检查。