【问题标题】:Updating cache without blocking不阻塞地更新缓存
【发布时间】:2014-03-25 22:00:33
【问题描述】:

我目前有一个具有类似缓存机制的程序。我有一个线程正在侦听从另一台服务器到此缓存的更新。该线程将在收到更新时更新缓存。这是一些伪代码:

void cache::update_cache()
{
    cache_ = new std::map<std::string, value>();
    while(true)
    {
        if(recv().compare("update") == 0)
        {
            std::map<std::string, value> *new_info = new std::map<std::string, value>();
            std::map<std::string, value> *tmp;
            //Get new info, store in new_info
            tmp = cache_;
            cache_ = new_cache;
            delete tmp;              
        }
    }
}

std::map<std::string, value> *cache::get_cache()
{
    return cache_;
}

cache_ 正在同时从许多不同的线程中读取。我相信如果我的一个线程调用get_cache(),然后我的缓存更新,然后线程尝试访问存储的缓存,我将在这里遇到未定义的行为。

我正在寻找一种方法来避免这个问题。我知道我可以使用互斥体,但我不想阻止读取发生,因为它们必须尽可能低延迟,但如果需要,我可以走这条路。

我想知道这对于 unique_ptr 是否是一个很好的用例。我的理解是否正确,如果一个线程调用 get_cache,并且返回一个 unique_ptr 而不是标准指针,一旦所有具有旧版本缓存的线程都用它完成(即离开范围),该对象将被删除。

对于这种情况,使用 unique_ptr 是最佳选择,还是我没有想到的其他选择?

我们将不胜感激。

编辑:

我相信我在我的 OP 中犯了一个错误。我的意思是使用和传递一个 shared_ptr 而不是一个 unique_ptr 用于 cache_。当所有线程都使用 cache_ 完成时,shared_ptr 应该自行删除。

关于我的程序的一点点:我的程序是一个网络服务器,它将使用这些信息来决定返回什么信息。这是相当高的吞吐量(数千个请求/秒)每个请求都会查询一次缓存,因此告诉我的其他线程何时更新是没有问题的。我可以容忍稍微过时的信息,并且如果可能的话,我更愿意阻止我的所有线程执行。缓存中的信息相当大,因此我想限制任何副本的值。

update_cache 只运行一次。它在只监听更新命令并运行代码的线程中运行。

【问题讨论】:

  • 使用readers-writer lockBoost provides one。同样,您可以使用来自here 的 RCU 的相同建议也适用。
  • 这是绝对可行的,但是,如果可能的话,我想避免阻塞。使用唯一指针会导致某种未定义的行为吗?为什么读/写锁是更好的解决方案?另外请注意,我可以容忍稍微过时的信息。
  • 我想我的疑虑实际上与设计有关,而不是与实现有关。首先,unique_ptr 似乎不起作用,因为只有一个线程可以合法地持有unique_ptr,这会破坏多个线程访问缓存。即使您可以解决这个问题,客户端线程如何知道它们需要更新?我觉得您的缓存应该公开一个接口,而不仅仅是将实际的缓存指针转储到客户端并让它们疯狂运行。如果您可以让客户端查询缓存,那么类似 RCU 的方法可能是完美的——零开销读取!
  • 哎呀,对不起,我的错误。我相信我的意思是shared_ptr 不是unique_ptr。我对我的原始帖子进行了一些更新,以更好地描述我的程序。我查询缓存的线程不需要知道缓存何时更新,它们正在处理 Web 请求,并且将使用提供的任何信息。我可以提供一个接口,但是数据集很大,我必须通过引用传递才能有效。所以我想要某种智能指针来处理我正在使用的任何数据(无论是值上的 smart_ptr 并传递它,还是地图上的 smart_ptr。

标签: c++ multithreading c++11 shared-ptr


【解决方案1】:

我觉得有多个问题:

1) 不要泄漏内存:因为永远不要在您的代码中使用“delete”并坚持使用 unique_ptr(或在特定情况下为 shared_ptr)

2) 使用锁定(互斥)或无锁机制(std::atomic)保护对共享数据的访问

class Cache {
    using Map = std::map<std::string, value>();
    std::unique_ptr<Map> m_cache;
    std::mutex m_cacheLock;
public:

    void update_cache()
    {
        while(true)
        {
            if(recv().compare("update") == 0)
            {
                std::unique_ptr<Map> new_info { new Map };
                //Get new info, store in new_info
                {
                   std::lock_guard<std::mutex> lock{m_cacheLock};
                   using std::swap;
                   swap(m_cache, new_cache);
                }
            }
        }
    }

注意:我不喜欢 update_cache() 成为缓存公共接口的一部分,因为它包含无限循环。我可能会用 recv 将循环外部化并有一个:

    void update_cache(std::unique_ptr<Map> new_info)
    {
        { // This inner brace is not useless, we don't need to keep the lock during deletion
           std::lock_guard<std::mutex> lock{m_cacheLock};
           using std::swap;
           swap(m_cache, new_cache);
        }
    }

现在为了读取缓存,使用适当的封装并且不要留下指向成员映射转义的指针:

    value get(const std::string &key)
    {
        // lock, fetch, and return. 
        // Depending on value type, you might want to allocate memory
        // before locking
    }

使用此签名,如果缓存中不存在该值,则必须抛出异常,另一种选择是返回类似 boost::optional 的内容。

总体而言,如果您在锁定部分之外进行昂贵的操作(例如内存分配),您可以保持低延迟(一切都是相对的,我不知道您的用例)。

【讨论】:

  • 这看起来确实可行。请参阅我的编辑以获得更多说明。基于此,您是否认为有任何理由将循环外部化?您的锁定和交换看起来会起作用。我没有添加接口来获取值的原因有几个。 1)我不想按值返回,它是一个大对象,我想按引用返回。 2)如果我返回一个值的引用,然后交换指针,我相信该引用会导致未定义的行为。我相信我在最初的帖子中犯了一个错误,我应该使用并返回一个 shared_ptr 用于 cache_。
  • 是的,如果 value 是一个大对象并且您必须避免复制,那么返回 shared_ptr 可能会更好。您仍然可以将缓存定义为 std::map<:string std::shared_ptr>>,并从 get 返回 std::shared_ptr。我仍然不会将具有此名称的线程方法作为缓存接口的一部分,我会将类分成两部分。我想知道为什么您的缓存更新不是增量的:一次仅添加/删除一个条目,而不是使完整缓存无效以获取全新的条目。特别是对于您提到的用例。
  • 这就是我的数据的性质。我的数据存在这样的情况:单个元素可以从单个键失效,或者一组元素可以从多个键失效。 Key 也将内部数据组织到预先查询的桶中,所以我并不总是知道哪些值在哪些桶中。值也是一组必须迭代的数据,使我的所有密钥保持最新的开销将超过仅重新查询数据的简单性。我想我可以使用一个接口返回一个 shared_ptr 值,它听起来确实更整洁,因为线程一次只使用一个键。
【解决方案2】:

shared_ptr 用于此目的非常合理,C++11 有a family of functions for handling shared_ptr atomically。如果数据在创建后是不可变的,您甚至不需要任何额外的同步:

class cache {
public:
    using map_t = std::map<std::string, value>;
    void update_cache();
    std::shared_ptr<const map_t> get_cache() const;
private:
    std::shared_ptr<const map_t> cache_;
};

void cache::update_cache()
{
    while(true)
    {
        if(recv() == "update")
        {
            auto new_info = std::make_shared<map_t>();
            // Get new info, store in new_info
            // Make immutable & publish
            std::atomic_store(&cache_,
                              std::shared_ptr<const map_t>{std::move(new_info)});
        }
    }
}

auto cache::get_cache() const -> std::shared_ptr<const map_t> {
    return std::atomic_load(&cache_);
}

【讨论】:

  • 谢谢,我相信这就是我想要的。对我来说不幸的是,我的编译器不支持 std::atomic_store/atomic_load 共享指针(运行 GCC 4.7.2),我无法升级我的编译器(在 CentOS 上运行)。我把它全部翻译了以提高等价物。到目前为止,它似乎工作正常。
猜你喜欢
  • 2017-09-08
  • 2015-11-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-16
  • 1970-01-01
  • 1970-01-01
  • 2015-07-20
相关资源
最近更新 更多