如何在 C# 中包含对象列表的字典上使用锁?

发布于 2024-10-24 12:45:41 字数 1197 浏览 1 评论 0原文

我有以下课程:

public static class HotspotsCache
{
  private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<int, List<HotSpot>>();
  private static object Lock = new object();


  public static List<HotSpot> GetCompanyHotspots(short companyId)
  {
     lock (Lock)
     {
       if (!_companyHotspots.ContainsKey(companyId))
       {
         RefreshCompanyHotspotCache(companyId);
       }

       return _companyHotspots[companyId];
     }
  }

  private static void RefreshCompanyHotspotCache(short companyId)
  {
    ....

    hotspots = ServiceProvider.Instance.GetService<HotspotsService>().GetHotSpots(..);
    _companyHotspots.Add(companyId, hotspots);

   ....
  }

我遇到的问题是在 RefreshCompanyHotspotCache 方法中获取热点的操作需要花费大量时间。因此,当一个线程正在对某个 CompanyId 执行缓存刷新时,所有其他线程都会等待该操作完成,尽管可能有线程正在请求另一个 companyId 的热点列表,而该列表已加载到该 CompanyId 中。字典。我希望最后这些线程不被锁定。我还希望请求尚未加载到缓存中的公司的热点列表的所有线程都等待,直到列表完全检索并加载到字典中。

有没有办法只锁定正在读取/写入特定 companyId(正在刷新)的缓存的线程,并让正在请求另一家公司数据的其他线程完成其工作?

我的想法是使用锁数组

lock (companyLocks[companyId])
{
...
}

但这并没有解决任何问题。与一家公司打交道的线程仍在等待为其他公司刷新缓存的线程。

I have the following class:

public static class HotspotsCache
{
  private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<int, List<HotSpot>>();
  private static object Lock = new object();


  public static List<HotSpot> GetCompanyHotspots(short companyId)
  {
     lock (Lock)
     {
       if (!_companyHotspots.ContainsKey(companyId))
       {
         RefreshCompanyHotspotCache(companyId);
       }

       return _companyHotspots[companyId];
     }
  }

  private static void RefreshCompanyHotspotCache(short companyId)
  {
    ....

    hotspots = ServiceProvider.Instance.GetService<HotspotsService>().GetHotSpots(..);
    _companyHotspots.Add(companyId, hotspots);

   ....
  }

The issue that I'm having is that the operation of getting the hotspots, in RefreshCompanyHotspotCache method, takes a lot of time . So while one thread is performing the cache refresh for a certain CompanyId, all the other threads are waiting until this operation is finished, although there could be threads that are requesting the list of hotspots for another companyId for which the list is already loaded in the dictionary. I would like these last threads not be locked. I also want that all threads that are requesting the list of hotspots for a company that is not yet loaded in the cache to wait until the list is fully retrieved and loaded in the dictionary.

Is there a way to lock only the threads that are reading/writing the cache for certain companyId (for which the refresh is taking place) and let the other threads that are requesting data for another company to do their job?

My thought was to use and array of locks

lock (companyLocks[companyId])
{
...
}

But that didn't solve anything. The threads dealing with one company are still waiting for threads that are refreshing the cache for other companies.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

梦一生花开无言 2024-10-31 12:45:41

使用 Snowbear 也提到的双重检查锁定机制 - 这将防止您的代码在实际上不需要时锁定。

根据您为每个客户端分配一个锁的想法,我过去使用过这种机制,尽管我使用了锁字典。我制作了一个实用程序类,用于从钥匙获取锁定对象:

/// <summary>
/// Provides a mechanism to lock based on a data item being retrieved
/// </summary>
/// <typeparam name="T">Type of the data being used as a key</typeparam>
public class LockProvider<T> 
{
    private object _syncRoot = new object();
    private Dictionary<T, object> _lstLocks = new Dictionary<T, object>();

    /// <summary>
    /// Gets an object suitable for locking the specified data item
    /// </summary>
    /// <param name="key">The data key</param>
    /// <returns></returns>
    public object GetLock(T key)
    {
        if (!_lstLocks.ContainsKey(key))
        {
            lock (_syncRoot)
            {
                if (!_lstLocks.ContainsKey(key))
                    _lstLocks.Add(key, new object());
            }
        }
        return _lstLocks[key];
    }
}

所以只需按以下方式使用它......

private static LockProvider<short> _clientLocks = new LockProvider<short>();
private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<short, List<HotSpot>>();

  public static List<HotSpot> GetCompanyHotspots(short companyId)
  {
      if (!_companyHotspots.ContainsKey(companyId)) 
      {
          lock (_clientLocks.GetLock(companyId)) 
          {
              if (!_companyHotspots.ContainsKey(companyId))
              {
                   // Add item to _companyHotspots here...
              }
      }
      return _companyHotspots[companyId];
  }

Use the Double-checked lock mechanism also mentioned by Snowbear - this will prevent your code locking when it doesn't actually need to.

With your idea of an individual lock per client, I've used this mechanism in the past, though I used a dictionary of locks. I made a utility class for getting a lock object from a key:

/// <summary>
/// Provides a mechanism to lock based on a data item being retrieved
/// </summary>
/// <typeparam name="T">Type of the data being used as a key</typeparam>
public class LockProvider<T> 
{
    private object _syncRoot = new object();
    private Dictionary<T, object> _lstLocks = new Dictionary<T, object>();

    /// <summary>
    /// Gets an object suitable for locking the specified data item
    /// </summary>
    /// <param name="key">The data key</param>
    /// <returns></returns>
    public object GetLock(T key)
    {
        if (!_lstLocks.ContainsKey(key))
        {
            lock (_syncRoot)
            {
                if (!_lstLocks.ContainsKey(key))
                    _lstLocks.Add(key, new object());
            }
        }
        return _lstLocks[key];
    }
}

So simply use this in the following manner...

private static LockProvider<short> _clientLocks = new LockProvider<short>();
private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<short, List<HotSpot>>();

  public static List<HotSpot> GetCompanyHotspots(short companyId)
  {
      if (!_companyHotspots.ContainsKey(companyId)) 
      {
          lock (_clientLocks.GetLock(companyId)) 
          {
              if (!_companyHotspots.ContainsKey(companyId))
              {
                   // Add item to _companyHotspots here...
              }
      }
      return _companyHotspots[companyId];
  }
风吹过旳痕迹 2024-10-31 12:45:41

您只锁定 1 个线程并让其更新,而其他人都使用旧列表怎么样?

private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<short, List<HotSpot>>();
private static Dictionary<short, List<HotSpot>> _companyHotspotsOld = new Dictionary<short, List<HotSpot>>();
private static bool _hotspotsUpdating = false;
private static object Lock = new object();

public static List<HotSpot> GetCompanyHotspots(short companyId)
{
    if (!_hotspotsUpdating)
    {
        if (!_companyHotspots.ContainsKey(companyId))
        {
            lock (Lock)
            {
                _hotspotsUpdating = true;
                _companyHotspotsOld = _companyHotspots;
                RefreshCompanyHotspotCache(companyId);

                _hotspotsUpdating = false;
                return _companyHotspots[companyId];
            }
        }
        else
        {
            return _companyHotspots[companyId];
        }
    }
    else
    {
        return _companyHotspotsOld[companyId];
    }
}

How about you only lock 1 thread, and let that update, while everyone else uses the old list?

private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<short, List<HotSpot>>();
private static Dictionary<short, List<HotSpot>> _companyHotspotsOld = new Dictionary<short, List<HotSpot>>();
private static bool _hotspotsUpdating = false;
private static object Lock = new object();

public static List<HotSpot> GetCompanyHotspots(short companyId)
{
    if (!_hotspotsUpdating)
    {
        if (!_companyHotspots.ContainsKey(companyId))
        {
            lock (Lock)
            {
                _hotspotsUpdating = true;
                _companyHotspotsOld = _companyHotspots;
                RefreshCompanyHotspotCache(companyId);

                _hotspotsUpdating = false;
                return _companyHotspots[companyId];
            }
        }
        else
        {
            return _companyHotspots[companyId];
        }
    }
    else
    {
        return _companyHotspotsOld[companyId];
    }
}
橘虞初梦 2024-10-31 12:45:41

您研究过 ReaderWriterLockSlim 吗?这应该能够让您获得更细粒度的锁定,只在需要时才采用写锁。

您可能需要注意的另一件事是虚假共享。我不知道锁是如何准确实现的,但是如果您锁定数组中的对象,它们在内存中必然彼此靠近,可能会将它们放在同一个缓存行上,因此锁可能不会按照您的预期运行。

另一个想法是,如果您将最后一个代码片段更改为

object l = companyLocks[companyId];
lock(l){

}

锁语句,那么会发生什么,这可能比预期的更多。

国杰

Have you looked into ReaderWriterLockSlim? That should be able to let get finer grained locking where you only take a writelock when needed.

Another thing you may need to look out for is false sharing. I don't know how a lock is implemented exactly but if you lock on objects in an array they're bound to be close to each other in memory, possibly putting them on the same cacheline, so the lock may not behave as you expect.

Another idea, what happens if you change the last code snippet to

object l = companyLocks[companyId];
lock(l){

}

could be the lock statement wraps more here than intended.

GJ

后eg是否自 2024-10-31 12:45:41

新想法,仅在创建列表时锁定列表。

如果你能保证每个公司至少有一个热点,那么可以这样做:

public static class HotspotsCache
{
    private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<int, List<HotSpot>>();

    static HotspotsCache()
    {
        foreach(short companyId in allCompanies)
        {
            companyHotspots.Add(companyId, new List<HotSpot>());
        }
    }

    public static List<HotSpot> GetCompanyHotspots(short companyId)
    {
        List<HotSpots> result = _companyHotspots[companyId];

        if(result.Count == 0)
        {
            lock(result)
            {
                if(result.Count == 0)
                {
                    RefreshCompanyHotspotCache(companyId, result);
                }
            }
        }

        return result;
    }

    private static void RefreshCompanyHotspotCache(short companyId, List<HotSpot> resultList)
    {
        ....

        hotspots = ServiceProvider.Instance.GetService<HotspotsService>().GetHotSpots(..);
        resultList.AddRange(hotspots);

        ....
    }
}

由于字典在初始创建后正在修改,因此不需要对其进行任何锁定。我们只需要在填充各个列表时锁定它们,读取操作不需要锁定(包括初始 Count == 0)。

New idea, with locking just the lists as they are created.

If you can guarantee that each company will have at least one hotspot, do this:

public static class HotspotsCache
{
    private static Dictionary<short, List<HotSpot>> _companyHotspots = new Dictionary<int, List<HotSpot>>();

    static HotspotsCache()
    {
        foreach(short companyId in allCompanies)
        {
            companyHotspots.Add(companyId, new List<HotSpot>());
        }
    }

    public static List<HotSpot> GetCompanyHotspots(short companyId)
    {
        List<HotSpots> result = _companyHotspots[companyId];

        if(result.Count == 0)
        {
            lock(result)
            {
                if(result.Count == 0)
                {
                    RefreshCompanyHotspotCache(companyId, result);
                }
            }
        }

        return result;
    }

    private static void RefreshCompanyHotspotCache(short companyId, List<HotSpot> resultList)
    {
        ....

        hotspots = ServiceProvider.Instance.GetService<HotspotsService>().GetHotSpots(..);
        resultList.AddRange(hotspots);

        ....
    }
}

Since the dictionary is being modified after its initial creation, no need to do any locking on it. We only need to lock the individual lists as we populate them, the read operation needs no locking (including the initial Count == 0).

○闲身 2024-10-31 12:45:41

如果您能够使用 .NET 4,那么答案很简单 - 使用 ConcurrentDictionary相反,让它为您处理并发详细信息:

public static class HotSpotsCache
{
    private static readonly ConcurrentDictionary<short, List<HotSpot>>
        _hotSpotsMap = new ConcurrentDictionary<short, List<HotSpot>>();

    public static List<HotSpot> GetCompanyHotSpots(short companyId)
    {
        return _hotSpotsMap.GetOrAdd(companyId, id => LoadHotSpots(id));
    }

    private static List<HotSpot> LoadHotSpots(short companyId)
    {
        return ServiceProvider.Instance
                              .GetService<HotSpotsService>()
                              .GetHotSpots(/* ... */);
    }
}

如果您无法使用 .NET 4,那么您使用多个更细粒度的锁的想法是一个好的:

public static class HotSpotsCache
{
    private static readonly Dictionary<short, List<HotSpot>>
        _hotSpotsMap = new Dictionary<short, List<HotSpot>();

    private static readonly object _bigLock = new object();
    private static readonly Dictionary<short, object>
        _miniLocks = new Dictionary<short, object>();

    public static List<HotSpot> GetCompanyHotSpots(short companyId)
    {
        List<HotSpot> hotSpots;
        object miniLock;
        lock (_bigLock)
        {
            if (_hotSpotsMap.TryGetValue(companyId, out hotSpots))
                return hotSpots;

            if (!_miniLocks.TryGetValue(companyId, out miniLock))
            {
                miniLock = new object();
                _miniLocks.Add(companyId, miniLock);
            }
        }
        lock (miniLock)
        {
            if (!_hotSpotsMap.TryGetValue(companyId, out hotSpots))
            {
                hotSpots = LoadHotSpots(companyId);
                lock (_bigLock)
                {
                    _hotSpotsMap.Add(companyId, hotSpots);
                    _miniLocks.Remove(companyId);
                }
            }
            return hotSpots;
        }
    }

    private static List<HotSpot> LoadHotSpots(short companyId)
    {
        return ServiceProvider.Instance
                              .GetService<HotSpotsService>()
                              .GetHotSpots(/* ... */);
    }
}

If you're able to use .NET 4 then the answer is straightforward -- use a ConcurrentDictionary<K,V> instead and let that look after the concurrency details for you:

public static class HotSpotsCache
{
    private static readonly ConcurrentDictionary<short, List<HotSpot>>
        _hotSpotsMap = new ConcurrentDictionary<short, List<HotSpot>>();

    public static List<HotSpot> GetCompanyHotSpots(short companyId)
    {
        return _hotSpotsMap.GetOrAdd(companyId, id => LoadHotSpots(id));
    }

    private static List<HotSpot> LoadHotSpots(short companyId)
    {
        return ServiceProvider.Instance
                              .GetService<HotSpotsService>()
                              .GetHotSpots(/* ... */);
    }
}

If you're not able to use .NET 4 then your idea of using several more granular locks is a good one:

public static class HotSpotsCache
{
    private static readonly Dictionary<short, List<HotSpot>>
        _hotSpotsMap = new Dictionary<short, List<HotSpot>();

    private static readonly object _bigLock = new object();
    private static readonly Dictionary<short, object>
        _miniLocks = new Dictionary<short, object>();

    public static List<HotSpot> GetCompanyHotSpots(short companyId)
    {
        List<HotSpot> hotSpots;
        object miniLock;
        lock (_bigLock)
        {
            if (_hotSpotsMap.TryGetValue(companyId, out hotSpots))
                return hotSpots;

            if (!_miniLocks.TryGetValue(companyId, out miniLock))
            {
                miniLock = new object();
                _miniLocks.Add(companyId, miniLock);
            }
        }
        lock (miniLock)
        {
            if (!_hotSpotsMap.TryGetValue(companyId, out hotSpots))
            {
                hotSpots = LoadHotSpots(companyId);
                lock (_bigLock)
                {
                    _hotSpotsMap.Add(companyId, hotSpots);
                    _miniLocks.Remove(companyId);
                }
            }
            return hotSpots;
        }
    }

    private static List<HotSpot> LoadHotSpots(short companyId)
    {
        return ServiceProvider.Instance
                              .GetService<HotSpotsService>()
                              .GetHotSpots(/* ... */);
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文