Search code examples
c++multithreadingcachinglru

How to implement thread-safe LRU cache eviction?


I've implemented an LRU cache (code) that I would like to use for a multithreaded matching problem with N elements and full N^2 (all pairs) matching. Ideally, I would just get a reference to each element directly from the cache to save memory.

The time is takes to match two elements (lets call them A and B) can greatly vary, and I am worried that if one pair of elements takes a long time to match then another thread (that is working very fast and processing many pairs) will cause either A or B to be evicted from the cache, making the references invalid.

A simple solution is to just not use references, but I'm wondering if there is a better way to ensure that elements won't be evicted if they are "currently used" or have a reference to them?


Solution

  • To avoid evicting objects that are in use it is possible to use the reference-counting functionality of std::shared_ptr. Consider the following implementation:

    #include <iostream>
    #include <string>
    #include <memory>
    #include <map>
    #include <algorithm>
    
    template <typename K, typename V> class cache
    {
    public:
        cache() {}
    
        static const constexpr int max_cache_size = 2;
    
        std::shared_ptr<V> getValue(const K& k)
        {
            auto iter = cached_values.find(k);
            if (iter == cached_values.end()) {
    
                if (cached_values.size() == max_cache_size) {
                    auto evictIter =
                        std::find_if(cached_values.begin(), cached_values.end(),
                            [](const auto& kv) { return kv.second.second.unique(); });
    
                    if (evictIter == cached_values.end()) {
                        std::cout << "Nothing to evict\n";
                        return nullptr;
                    }
    
                    cached_values.erase(evictIter);
                }
    
                static V next;
    
                iter = cached_values.insert(std::make_pair(k, std::make_pair(++next, nullptr))).first;
                iter->second.second = std::shared_ptr<V>(&iter->second.first, [](const auto&) {});
            }
    
            return iter->second.second;
        }
    
        std::map<K, std::pair<V, std::shared_ptr<V>>> cached_values;
    };
    
    int main()
    {
        cache<int, int> c;
    
        std::cout << *c.getValue(10) << "\n";
        std::cout << *c.getValue(20) << "\n";
        std::cout << *c.getValue(30) << "\n";
    
        auto useOne = c.getValue(10);
        auto useTwo = c.getValue(20);
        std::cout << *c.getValue(20) << "\n"; // We can use stuff that is still in cache
        std::cout << c.getValue(30);          // Cache is full, also note no dereferencing
    }
    

    Basically, as long as anyone outside the cache uses the returned value, the std::shared_ptr::unique will return false, making the cache entry non-evictable.

    Live example