【问题标题】:Synchronize threads on per-item base在每个项目基础上同步线程
【发布时间】:2016-04-25 11:22:39
【问题描述】:

虽然这个问题是关于 MemoryCache 类的,但我可以想象与 Dictionary 或 ConcurrentDictionary.GetOrAdd 相同的需求,其中 valueFactory-lambda 也是一个冗长的操作。

本质上,我想在每个项目的基础上同步/锁定线程。我知道MemoryCache 是线程安全的,但是仍然需要检查一个项目是否存在并在它不存在时添加该项目,仍然需要同步。

考虑这个示例代码:

public class MyCache
{
    private static readonly MemoryCache cache = new MemoryCache(Guid.NewGuid().ToString());

    public object Get(string id)
    {
        var cacheItem = cache.GetCachedItem(id);
        if (cacheItem != null) return cacheItem.Value;
        var item = this.CreateItem(id);
        cache.Add(id, item, new CacheItemPolicy
        {
            SlidingExpiration = TimeSpan.FromMinutes(20)
        });
        return item;
    }

    private object CreateItem(string id)
    {
        // Lengthy operation, f.e. querying database or even external API
        return whateverCreatedObject;
    }
}

如您所见,我们需要同步cache.GetCachedItem 和cache.Add。但是由于CreateItem 是一个冗长的操作(因此是MemoryCache),我不想像这段代码那样锁定所有线程:

public object Get(string id)
{
    lock (cache)
    {
        var item = cache.GetCachedItem(id);
        if (item != null) return item.Value;
        cache.Add(id, this.CreateItem(id), new CacheItemPolicy
        {
            SlidingExpiration = TimeSpan.FromMinutes(20)
        });
    }
}

此外,没有锁也不是一种选择,因为这样我们可以有多个线程为同一个id 调用CreateItem。

我可以做的是为每个 id 创建一个名为 Semaphore 的唯一名称,因此锁定是在每个项目的基础上发生的。但这将是一个系统资源杀手,因为我们不想在我们的系统上注册 +100K 命名信号量。

我确定我不是第一个需要这种同步的人,但我没有找到任何适合这种情况的问题/答案。

我的问题是,是否有人可以为这个问题想出一种不同的、资源友好的方法?

更新

我发现this NamedReaderWriterLocker 类起初看起来很有希望,但使用起来很危险,因为当两个线程都进入ConcurrentDictionary 的@987654341 时,两个线程可能会获得同名的不同ReaderWriterLockSlim 实例@ 同时。也许我可以在GetLock 方法中使用这个实现和一些额外的锁。

【问题讨论】:

  • ConcurrentDictionary 在其 API 中使用 Try*/*Or* 模式:msdn.microsoft.com/en-us/library/dd287191%28v=vs.110%29.aspx
  • 是的,GetOrAdd 有点像,但我敢打赌 ConcurrentDictionary 在执行 add lambda valueFactory 时也会锁定整个字典,并且不使用每键锁定。
  • AFAIK,ConcurrentDictionary 是无锁的。
  • 经过测试,确实是无锁的。测试表明,第二个GetOrAdd 的valueFactory 也被调用,而第一个GetOrAdd 的valueFactory 仍在进行中,即使我们正在尝试获取相同的项目(相同的密钥)。所以,我想要的是第二个GetOrAdd 调用等待第一个调用完成(但前提是我们当然要尝试获取相同的项目)。
  • “等待”意味着一些锁定。无需等待,就有可能调用 value factory 两次,你是绝对正确的。恐怕,无论实施如何,您都必须在这两种情况之间做出选择。

标签: c# multithreading synchronization locking semaphore


【解决方案1】:

由于您的密钥是字符串,您可以锁定string.Intern(id)。

MSDN 文档:System.String.Intern

即

lock (string.Intern(id))
{
    var item = cache.GetCachedItem(id);
    if (item != null)
    {
        return item.Value;
    }

    cache.Add(id, this.CreateItem(id), new CacheItemPolicy
    {
        SlidingExpiration = TimeSpan.FromMinutes(20)
    });

   return /* some value, this line was absent in the original code. */;
}

【讨论】:

  • 不知道string.Intern的存在,很有趣。另一方面,我认为这是一个非常棘手的方法。
  • 是的,它会给你留下内存开销,直到进程退出才会消失。
  • 这是一种相当可靠的方法,当您可以确定这些字符串上没有其他锁定时。开销通常应该是可以接受的。
  • 我可以在 id 前面加上类名或唯一键以避免锁冲突。
猜你喜欢
  • 2020-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-09
  • 1970-01-01
  • 1970-01-01
  • 2018-03-28
相关资源
最近更新 更多