【问题标题】:how to use async method inside lock如何在锁内使用异步方法
【发布时间】:2020-01-17 20:30:45
【问题描述】:

我有 CacheService,它在 MemoryCache 中存储一个集合,然后从存储在缓存中的集合中找到一个元素。鉴于多线程环境,我想确保只有一个 Worker 可以将集合存储在缓存中并找到它。所以我使用lock 来同步调用并使线程安全。

public class MyCacheService
{
    private readonly IMemoryCache _memoryCache = null;
    static object myLock = new object();

    public MyCacheService(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
    }       

    public async Task<Job> Find(int key, string title, int[] skills, Func<int, Task<List<Job>>> getJobs)
    {
        lock (myLock)
        {
            List<Job> cachedJobs = null;
            if (!_memoryCache.TryGetValue(key, out cachedJobs))
            {
                // compilation error here 'cannot await in the body of a lock statement'
                var jobs = await getJobs(key);

                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    .SetSlidingExpiration(TimeSpan.FromMinutes(30));
                cachedJobs = _memoryCache.Set(key, cachedJobs, cacheEntryOptions);
            }

            if (cachedJobs != null)
            {
                var job = cachedJobs.Where(j => j.Title == title &&
                                   !j.Skills.Except(skills).Any())
                              .FirstOrDefault();

                if (job == null)
                {
                    return null;
                }

                cachedJobs.Remove(job);
                return job;
            }

            return null;
        }
    }
}

getJobs 委托是从数据库中获取作业的异步调用。所以我收到错误cannot await in the body of a lock statement

我明白为什么我会出错。我可以使用getJobs(key).GetAwaiter().GetResult() 来解决错误

LazyCache 保证对要缓存其结果的委托进行单一评估,我们可以使用异步委托,但我没有使用它

还有其他选择吗?

更新 1
我尝试按照建议使用SemaphoreSlim,但它没有按预期工作。在 下面的 DEMO 我总共有 10000 个工作(5000 个 BASIC 工作和 5000 个 Master 工作)和总共 200 个工人。前 100 个工人 (1-100) 用于 BASIC 作业,101 到 200 个工人用于 Master 作业。

期望从 1 到 100 的任何工人都将获得 BASIC 工作,而 101-200 将获得 MASTER 工作

SemaphoreSlim 似乎没有按预期工作。使用这种方法,所有 5000 个 BASIC 作业总是分配给 ID 为 1 的 Worker。并且所有 MASTER 作业总是被分配给 ID 为 101 的 Worker

DEMO 使用 SemaphoreSlim

只要我没有在锁内使用异步方法,我使用 C# 锁的初始方法似乎可以按预期工作

DEMO 使用 lock

【问题讨论】:

  • GetAwaiter().GetResult() 不是解决此问题的好方法,因为您现在正在阻塞异步结果,这完全破坏了异步过程。您应该避免在等待结果时锁定某些东西,但如果这样做,您可以使用其他一些锁定机制,例如SemaphoreSlim.
  • 另一种方法可能是不缓存实际结果,而是为结果缓存 tasks。因此,当您没有缓存值时,您正在存储任务,并且所有其他请求(您通常会阻止)将获得相同的结果任务。

标签: asp.net-core .net-core memorycache lazycache


【解决方案1】:

考虑到多线程环境,我想确保只有一个 Worker 可以将集合存储在缓存中并找到它。

您当前(尝试的)解决方案有一个非常粗略的锁定:如果一个请求尝试使用给定密钥查找作业,它可能会被另一个正在查询数据库以获取不同密钥的作业的请求阻止。也就是说,可以使用SempahoreSlim 对现有代码进行字面翻译:

  static SemaphoreSlim myLock = new SemaphoreSlim(1);

  public async Task<Job> Find(int key, string title, int[] skills, Func<int, Task<List<Job>>> getJobs)
  {
    await myLock.WaitAsync();
    try
    {
      ...
    }
    finally
    {
      myLock.Release();
    }
  }

【讨论】:

    【解决方案2】:

    您可以使用支持异步委托的 IDistributedCache。并使用Redis 进行缓存实现。

    Distributed caching in ASP.NET core

    【讨论】:

      【解决方案3】:

      @Stephen Cleary 的回答仍然有效且完美。您的演示代码并没有像您可能期望的那样在后台线程上执行所有工作任务,因此您基本上是在等待(每种作业类型的)第一个任务完成。

      在您的演示代码中,您可以将所有工作任务作为后台任务运行,如下所示:

      // your other code...
      workers.AddRange(masterWorkers);
                    
      var start = DateTime.Now;
      var tasks = workers.Select(s => Task.Run(s.DoWorkAsync)).ToArray();
      
      Task.WaitAll(tasks);
      
      var end = DateTime.Now;
      // your other code
      

      小提琴:https://dotnetfiddle.net/GPasHM

      Microsoft的文档中的示例中也是如此;虽然缺少try{} finally{} 的一般推荐?。

      【讨论】:

        猜你喜欢
        • 2021-11-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-03
        • 1970-01-01
        • 1970-01-01
        • 2022-12-17
        相关资源
        最近更新 更多