锁定和刷新实现

Lock and refresh implementation

我有任务映射的关键,只有当给定的任务尚未 运行ning 时,我才需要 运行 任务。伪代码如下。我相信还有很大的改进空间。我锁定在地图上,因此几乎序列化了对 CacheFreshener 的访问。有更好的方法吗?我们知道,当我尝试锁定键 k1 时,缓存刷新器调用键 k2 等待锁定是没有意义的。

class CacheFreshener
{
     private ConcurrentDictionary<string,bool> lockMap;

     public RefreshData(string key, Func<string, bool> cacheMissAction)
     {
         lock(lockMap)
         {
             if (lockMap.ContainsKey(key))
             {
                 // no-op
                 return;
             }
             else
             {
                lockMap.Add(key, true); 
             }  
         }

         // if you are here means task is not already present
         cacheMissAction(key);

         lock(lockMap) // Do we need to lock here??
         {
             lockMap.Remove(key);
         }  
     }  

}

根据要求,这里详细解释了我对我的评论的看法……

这里的基本问题似乎是并发问题,即两个或多个线程同时访问同一个对象。这是 ConcurrentDictionary 设计的场景。如果您分别使用 ContainsKey()Add()IDictionary 方法,那么您将需要显式同步(但仅针对该操作......在这种特定情况下,调用时并不严格需要Remove()) 以确保这些操作作为单个原子操作执行。但是 ConcurrentDictionary class 预料到这种需求,并包含 TryAdd() 方法来完成相同的操作,而无需显式同步。

<旁白>
我并不完全清楚给出的代码示例背后的意图。该代码似乎仅在 cacheMissAction 委托调用期间将对象存储在 "cache" 中。之后立即删除密钥。所以看起来它本身并没有真正缓存任何东西。它只是防止多个线程同时处于调用 cacheMissAction 的过程中(后续线程将无法调用它,但也不能指望它在调用 [=21= 时完成) ] 方法已完成)。

但是以给定的代码示例为例,很明显实际上不需要显式锁定。 ConcurrentDictionary class 已经提供了线程安全访问(即当从多个线程并发使用时数据结构不会损坏),并且它提供了 TryAdd() 方法作为添加一个机制键(和它的值,虽然这里总是 truebool 文字)到字典,这将确保一次只有一个线程在字典中有一个键。

因此我们可以将代码重写为如下所示并实现相同的目标:

private ConcurrentDictionary<string,bool> lockMap;

public RefreshData(string key, Func<string, bool> cacheMissAction)
{
    if (!lockMap.TryAdd(key, true))
    {
        return;
    }

    // if you are here means task was not already present
    cacheMissAction(key);

    lockMap.Remove(key);
}

添加或删除不需要 lock 语句,因为 TryAdd() 以原子方式处理整个 "check for key and add if not present" 操作。


我会注意到使用字典来完成集合的工作可能被认为是低效的。如果集合可能不大,那没什么大不了的,但我确实觉得奇怪,微软选择犯他们最初在前泛型时代你不得不使用非泛型字典对象时犯的同样的错误 Hashtable 来存储一组,在 HashSet<T> 出现之前。现在我们在 System.Collections.Concurrent 中拥有所有这些易于使用的 classes,但其中没有 ISet<T> 的线程安全实现。唉……

就是说,如果您 在存储方面更喜欢更有效的方法(这不一定是 更快 实施,取决于对象的并发访问模式),像这样的东西可以作为替代方案:

private HashSet<string> lockSet;
private readonly object _lock = new object();

public RefreshData(string key, Func<string, bool> cacheMissAction)
{
    lock (_lock)
    {
        if (!lockSet.Add(key))
        {
            return;
        }
    }

    // if you are here means task was not already present
    cacheMissAction(key);

    lock (_lock)
    {
        lockSet.Remove(key);
    }
}

在这种情况下,您确实需要 lock 语句,因为 HashSet<T> class 本身不是线程安全的。这当然与您的原始实现非常相似,只是使用 HashSet<T> 的更类似于集合的语义。