在每个项目基础上同步线程

Synchronize threads on per-item base

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

本质上,我想 synchronize/lock 基于每个项目的线程。我知道 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.GetCachedItemcache.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 命名信号量。

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

我的问题是,是否有人可以针对此问题提出一种不同的、资源友好的方法?

更新

我发现 this NamedReaderWriterLocker class 一开始看起来很有前途,但使用起来很危险,因为两个线程可能会获得同名的不同 ReaderWriterLockSlim 实例同时进入 ConcurrentDictionaryvalueFactory。也许我可以在 GetLock 方法中使用这个实现和一些额外的锁。

由于您的密钥是一个字符串,您可以锁定 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. */;
}