【问题标题】:Thread safety for high-performance in-memory cache高性能内存缓存的线程安全
【发布时间】:2011-11-19 16:12:36
【问题描述】:

我有一个静态内存缓存,每小时仅写入一次(或更长),并且由许多线程以极高的速率读取。传统观点建议我遵循如下模式:

public static class MyCache
{
    private static IDictionary<int, string> _cache;
    private static ReaderWriterLockSlim _sharedLock;

    static MyCache()
    {
        _cache = new Dictionary<int, string>();
        _sharedLock = new ReaderWriterLockSlim();
    }

    public static string GetData(int key)
    {
        _sharedLock.EnterReadLock();
        try
        {
            string returnValue;
            _cache.TryGetValue(key, out returnValue);
            return returnValue;
        }
        finally
        {
            _sharedLock.ExitReadLock();
        }
    }

    public static void AddData(int key, string data)
    {
        _sharedLock.EnterWriteLock();
        try
        {
            if (!_cache.ContainsKey(key))
                _cache.Add(key, data);
        }
        finally
        {
            _sharedLock.ExitWriteLock();
        }
    }
}

作为一个微优化的练习,我如何才能在共享 read 锁的相对开销中减少更多滴答声? 编写的时间可能很昂贵,因为这种情况很少发生。我需要尽可能快地进行读取。在这种情况下,我可以只删除 read 锁(如下)并保持线程安全吗?或者有没有我可以使用的无锁版本?我熟悉内存防护,但不知道如何在这种情况下安全地应用它。

注意:我不拘泥于任何一种模式,所以只要最终结果更快并且在 C# 4.x.*中,任何建议都是受欢迎的。*

public static class MyCache2
{
    private static IDictionary<int, string> _cache;
    private static object _fullLock;

    static MyCache2()
    {
        _cache = new Dictionary<int, string>();
        _fullLock = new object();
    }

    public static string GetData(int key)
    {
        //Note: There is no locking here... Is that ok?
        string returnValue;
        _cache.TryGetValue(key, out returnValue);
        return returnValue;
    }

    public static void AddData(int key, string data)
    {
        lock (_fullLock)
        {
            if (!_cache.ContainsKey(key))
                _cache.Add(key, data);
        }
    }
}

【问题讨论】:

  • 对于像这样的微优化,您确实必须首先进行分析。现在 ReaderLock 占用了多少 %?
  • 您的第二个版本不是线程安全的。写作时没有什么可以保护阅读。
  • 您准备在写入方面牺牲多少性能来换取读取性能?您可以制作一个非锁定版本,在需要添加数据时简单地替换字典。读取没有锁,但显然,在写入场景中要慢很多
  • driis... 在完全锁定到位时,写入的完全锁定不会阻止读取吗?
  • @JoeGeeky,怎么可能? GetData() 中的代码对锁一无所知,因此它不会以任何方式对其作出反应。

标签: c# performance thread-safety micro-optimization


【解决方案1】:

当只有线程从数据结构中读取时,您不需要锁。因此,由于写入如此罕见(并且,我假设不是并发的),一个选项可能是制作字典的完整副本,对副本进行修改,然后以原子方式将旧字典与新字典交换:

public static class MyCache2
{
    private static IDictionary<int, string> _cache;

    static MyCache2()
    {
        _cache = new Dictionary<int, string>();
    }

    public static string GetData(int key)
    {
        string returnValue;
        _cache.TryGetValue(key, out returnValue);
        return returnValue;
    }

    public static void AddData(int key, string data)
    {
        IDictionary<int, string> clone = Clone(_cache);
        if (!clone.ContainsKey(key))
            clone.Add(key, data);
        Interlocked.Exchange(ref _cache, clone);
    }
}

【讨论】:

  • 基于上面的其他一些 cmets,GetData() 方法在这种情况下不会是线程安全的。因此,我会重新使用某种锁定机制。这不是一个坏主意,但它似乎并没有解决读取性能问题。
  • 如果有线程操作_cache 引用的字典,GetData 将不是线程安全的。但是没有线程在操作它,所以它线程安全的!
  • 这是我最初的想法,但以前的 cmets 似乎不同意。无论如何,我现在正在尝试对此进行测试。谢谢。
  • 第二个示例中的 GetData 方法不是线程安全的,因为 AddData 正在操作 _cache 引用的字典。
  • 我认为这只有在我能保证没有两个线程同时尝试 AddData() 时才是安全的,否则,一个更新可能会丢失。就我而言,这不是问题,我只是想确保我理解这一点。
【解决方案2】:

我希望在这里实现无锁,并通过简单地不更改任何已发布的字典来实现线程安全。我的意思是:当您需要添加数据时,创建字典的完整副本,然后追加/更新/等副本。由于这是一个小时一次,即使对于大数据,这也不应该是一个问题。然后,当您进行更改后,只需将引用从旧字典交换到新字典(引用读/写保证是原子的)。

一个警告:任何需要在多个操作之间保持一致状态的代码都应该首先将字典捕获到一个变量中,即

var snapshot = someField;
// multiple reads on snapshot

这确保了所有相关逻辑都使用相同的版本数据,以避免在操作过程中交换引用时产生混淆。

我还会在写入时(而不是在读取时)使用锁,以确保不会对数据进行争吵。也有无锁的多写入器方法(主要是 Interlocked.CompareExchange 并在失败时重新应用),但我会先使用最简单的方法,而单个写入器正是如此。

【讨论】:

  • 关于捕获变量以保持操作之间的一致性。这是否适用于 if 条件中的一致性,例如if(someField == null || someField.SomeProp == null) someField 可以在条件之间切换吗?
【解决方案3】:

替代选项:.net 1.x 哈希表(本质上是字典,减去泛型)有一个有趣的线程故事;读取在没有锁的情况下是线程安全的 - 您只需要使用锁来确保最多一个写入器。

所以:您可以考虑使用非泛型 Hashtable,不锁定读取,然后在写入期间锁定。

这是我有时仍然使用 Hashtable 的主要原因,即使在 .net 4.x 应用程序中也是如此。

但有一个问题 - 它会导致 int 键被装箱以进行存储和查询。

【讨论】:

  • 有趣,我不知道这个...我将其添加到测试列表中,此时它变得很长:-)。但是拳击???哎哟!
  • @joe 是的 - 大多数情况下,密钥是字符串,所以这不是问题。但是每个查询的装箱将是 GEN-0 可收藏的,所以实际上可能不是什么大问题。
  • 测试在... Hashtable 表现非常好。在短时间内(例如,在我的硬件上不到 1 分钟),它完成了我在高并发率下测试的所有高性能读取。这包括无锁字典、并发字典等……但是,在持续负载下,它落后于 dtd 建议的无锁字典方法。定期发生的指标中有一个奇怪的滴答声,导致其长期表现略有下降。在某些情况下,所有事情都被认为是不错的选择。
  • @Joe 我想知道那个刻度是不是装箱的 imt 值的集合?
  • 这种方法的另一个好处是没有被抛出的 key not found 异常。因为我不必担心异常,所以不需要 try-catch。这意味着包含调用的方法仍然可以内联 JIT 优化。 :-)
【解决方案4】:

这只会在添加数据时复制字典。锁用于添加,但如果您不打算从多个线程添加,则可以将其取出。如果没有副本,则从原始字典中提取数据,否则在添加时使用副本。

以防副本在检查后被清空并被视为不为空,但在它能够检索值之前,我添加了一个 try catch,在这种罕见的事件中,它将从原始数据中提取数据,然后锁定,但同样,这种情况应该很少发生。

public static class MyCache2
{
    private static IDictionary<int, string> _cache;
    private static IDictionary<int, string> _cacheClone;
    private static Object _lock;

    static MyCache2()
    {
        _cache = new Dictionary<int, string>();
        _lock = new Object();
    }

    public static string GetData(int key)
    {
        string returnValue;
        if (_cacheClone == null)
        {
            _cache.TryGetValue(key, out returnValue);
        }
        else
        {
            try
            {
                _cacheClone.TryGetValue(key, out returnValue);
            }
            catch
            {
                lock (_lock)
                {
                    _cache.TryGetValue(key, out returnValue);
                }
            }
        }
        return returnValue;
    }

    public static void AddData(int key, string data)
    {
        lock (_lock)
        {
            _cacheClone = Clone(_cache);
            if (!_cache.ContainsKey(key))
                _cache.Add(key, data);
            _cacheClone = null;
        }
    }
}

【讨论】:

    【解决方案5】:

    您还可以查看无锁数据结构。 http://www.boyet.com/Articles/LockfreeStack.html 就是一个很好的例子

    【讨论】:

    • 这是一本非常有趣的读物……写得也很好。在这种情况下,这些都是不适合我上面介绍的情况的顺序访问结构。话虽如此,我现在正在启动一个性能装备,因为我很好奇他的无锁队列实现在内置 .NET 版本上的表现如何。 :-)
    • 按照承诺,我测试了无锁队列的实现。不幸的是,在每个测试用例中,无锁版本都比使用带有全锁的通用队列慢。几乎在所有情况下,全锁队列都快 2 倍。我喜欢这个主意,但数字说明了故事。
    • 忘了说,在处理少量项目时,您引导我进入的无锁队列比 Microsoft 的 ConcurrentQueue 更快。随着队列大小的增长,无锁队列落后,并发队列变得更快。对于小型队列,请继续使用带有完整锁的通用队列......在这种情况下它要快得多。
    猜你喜欢
    • 2012-06-22
    • 2018-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-15
    • 2012-07-11
    • 1970-01-01
    相关资源
    最近更新 更多