如何实现线程安全的 LRU 缓存驱逐?

Posted

技术标签:

【中文标题】如何实现线程安全的 LRU 缓存驱逐?【英文标题】:How to implement thread-safe LRU cache eviction? 【发布时间】:2015-10-12 19:47:39 【问题描述】:

我已经实现了一个 LRU 缓存 (code),我想将它用于具有 N 个元素和完整 N^2(所有对)匹配的多线程匹配问题。理想情况下,我会直接从缓存中获取对每个元素的引用以节省内存。

匹配两个元素(我们称它们为 A 和 B)所需的时间可能会有很大差异,我担心如果一对元素需要很长时间才能匹配,那么另一个线程(工作速度非常快并且正在处理许多对) 将导致 A 或 B 从缓存中逐出,使引用无效。

一个简单的解决方案是不使用引用,但我想知道是否有更好的方法来确保元素在“当前使用”或引用它们时不会被驱逐?

【问题讨论】:

如果没有关于您的键和值类型的任何信息,您的问题是无法回答的。一个明显的方法是存储并发送std::shared_ptr - 但要回答应该由谁创建、何时创建等,需要更多信息。 另外,你的整个班级都不是线程安全的。因此,无论您是否担心驱逐,它都可能会崩溃。因此,您应该从使用互斥锁(或您喜欢的任何同步方法)保护关键部分开始。 是的,我知道它目前不是线程安全的。开始添加线程安全时,我想到了这个问题。 我实际上不同意键/值部分。我在问是否对通用解决方案有任何见解,因为这是通用 LRU 缓存 对于一般键/值类型,使代码线程安全的唯一可能方法是在访问映射 cache_entries_map_ 时使用锁定。但是,如果您对使用的类型施加一些限制(例如,atomic<> 类型作为值,integer 类型作为键),我想可以使用一些特殊的 map 实现,这不会使用锁定(即成为无锁)。 【参考方案1】:

为避免驱逐正在使用的对象,可以使用std::shared_ptr 的引用计数功能。考虑以下实现:

#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

基本上,只要缓存之外的任何人使用返回值,std::shared_ptr::unique 就会返回 false,从而使缓存条目不可驱逐。

Live example

【讨论】:

非常感谢。关于std::shared_ptr 是正确的使用方式,您绝对是正确的。幸运的是,通过上面的实现,我可以将 shared_ptr 作为模板值传递

以上是关于如何实现线程安全的 LRU 缓存驱逐?的主要内容,如果未能解决你的问题,请参考以下文章

如何保证LinkedHashMap以及它实现LRU缓存线程安全

如何保证LinkedHashMap以及它实现LRU缓存线程安全

Oz/Mozart中类似线程安全的lru缓存。

LRU 实现缓存

#yyds干货盘点#使用线程安全型双向链表实现简单 LRU Cache 模拟

使用线程安全型双向链表实现简单 LRU Cache 模拟