高性能内存缓存的线程安全性

Joe*_*eky 5 c# performance thread-safety micro-optimization

我有一个静态内存缓存,只能写入一小时(或更长)一次,并被许多线程以极高的速率读取.传统观点认为我遵循以下模式:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

注意:我不依赖于任何一种模式,因此只要最终结果更快并且在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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

dtb*_*dtb 12

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我认为这只是安全的,如果我能保证没有两个线程同时尝试AddData(),否则,一个更新可能会丢失.在我的情况下,这不是问题,我只是想确保我理解这一点. (2认同)

Mar*_*ell 6

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

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

var snapshot = someField;
// multiple reads on snapshot
Run Code Online (Sandbox Code Playgroud)

这确保了所有相关逻辑都是使用相同版本的数据进行的,以避免在操作期间参考交换时出现混淆.

我也会在写入时(而不是在阅读时)锁定,以确保不会对数据进行争吵.还有无锁多写器方法(主要是Interlocked.CompareExchange,如果失败则重新应用),但我会首先使用最简单的方法,而单个编写器就是这样.