Skip to main content
5 of 6
edits don't need to be explicit
dfhwze
  • 13.9k
  • 3
  • 38
  • 101

Reducing contention in thread-safe LruCache

I was looking for a in-memory cache class, but couldn't find any in the .Net Core framework; luckily I found one already implemented here.

I made some minor modifications to it and tried to make it thread safe. I believe I succeeded; but boy, did I do a sloppy job? Just throwing locks around and whatnot.

I'd like to reduce thread contention and improve its performance. I considered using the ReadWriterLockSlim* class; one instances for the private dict and one for the private linked list.

Can someone help me improve the performance of this class?

For reference, at any given time there may be up to 40 threads hammering down this cache.

I can't provide a real and concise usage example of this class right now; and I have not yet measured the following numbers, but I expect that most 4/5 cache accesses will result in a hit, and 1/5 in a miss followed by an add.

An actual, yet codeless, example of usage of this class is to store the fitness of (immutable) individuals in a genetic algorithm. Since the individuals are immutable, their fitnesses stay the same as long as they live; thus I can compute it once and cache the result.

The most up-to-date version of the code can be found here. I'm currently trying to implement an elegant and low-contention TValue GetOrCreate(TKey key, Func<TValue> valueCreator) function; will update thi question when I'm done.

P.s.: I received such great feedback, it is kinda hard to choose one as the answer.

namespace Minotaur.Collections {
using System;
using System.Collections.Generic;

/// <remarks>
/// This class is based on the following inplementation https://stackoverflow.com/a/3719378/1642116
/// </remarks>
public sealed class LruCache<K, V> {
    private readonly int _capacity;
    private readonly Dictionary<K, LinkedListNode<LRUCacheEntry<K, V>>> _cacheMap;
    private readonly LinkedList<LRUCacheEntry<K, V>> _lruList = new LinkedList<LRUCacheEntry<K, V>>();
    private readonly object _lock = new object();

    /// <remarks>
    /// Providing the value 0 to <paramref name="capacity"/>
    /// effectively disables the cache.
    /// </remarks>
    public LruCache(int capacity) {
        if (capacity < 0)
            throw new ArgumentOutOfRangeException(nameof(capacity) + " must be >= 0");

        this._capacity = capacity;
        this._cacheMap = new Dictionary<K, LinkedListNode<LRUCacheEntry<K, V>>>(capacity: capacity);
    }

    /// <remarks>
    /// This method is thread-safe.
    /// </remarks>
    public void Add(K key, V value) {
        if (key == null)
            throw new ArgumentNullException(nameof(key));

        lock (_lock) {
            if (_capacity == 0)
                return;

            if (_cacheMap.Count >= _capacity)
                RemoveLastRecentlyUsed();

            var cacheItem = new LRUCacheEntry<K, V>(key, value);
            var node = new LinkedListNode<LRUCacheEntry<K, V>>(cacheItem);
            _lruList.AddLast(node);
            _cacheMap.Add(key, node);
        }
    }

    /// <remarks>
    /// This method is thread-safe.
    /// </remarks>
    public bool TryGet(K key, out V value) {
        if (key == null)
            throw new ArgumentNullException(nameof(key));

        lock (_lock) {
            var isCached = _cacheMap.TryGetValue(key, out var node);

            if (!isCached) {
                value = default;
                return false;
            }

            // Updating the last recently used thingy
            _lruList.Remove(node);
            _lruList.AddLast(node);

            value = node.Value.Value;
            return true;
        }
    }

    private void RemoveLastRecentlyUsed() {
        // Remove from lru list
        var node = _lruList.First;
        _lruList.RemoveFirst();

        // Remove from cache
        _cacheMap.Remove(node.Value.Key);
    }

    private sealed class LRUCacheEntry<KeyType, ValueType> {
        public readonly KeyType Key;
        public readonly ValueType Value;

        public LRUCacheEntry(KeyType k, ValueType v) {
            Key = k;
            Value = v;
        }
    }
}

}

*then I remembered I don't really know how to use it correctly and would probably end up increasing contention...

Trauer
  • 801
  • 6
  • 17