无锁重载和共享 const 对象

Posted

技术标签:

【中文标题】无锁重载和共享 const 对象【英文标题】:Lockfree Reloading and Sharing of const objects 【发布时间】:2016-10-25 11:36:42 【问题描述】:

我的应用程序的配置是一个与多个线程共享的 const 对象。配置存储在一个集中位置,任何线程都可以访问它。我已经尝试构建一个无锁实现,它允许我加载新配置,同时仍然允许其他线程读取最后一个已知配置。

我当前的实现在更新 shared_ptr 和读取它之间存在竞争。

template<typename T>
class ConfigurationHolder

public:
    typedef std::shared_ptr<T> SPtr;
    typedef std::shared_ptr<const T> CSPtr;

    ConfigurationHolder() : m_active(new T()) 

    CSPtr get() const  return m_active;  // RACE - read

    template<typename Reloader>
    bool reload(Reloader reloader)
    
        SPtr tmp(new T());
        if (!tmp)
            return false;
        if (!reloader(tmp))
            return false;
        m_active=tmp; // RACE - write
        return true;
    

private:
    CSPtr m_active;
;

我可以添加一个shared_mutex 来解决对shared_ptr 的有问题的读/写访问,但我正在寻找一种能够保持实现无锁的解决方案。

编辑:我的 GCC 版本不支持 atomic_exchange shared_ptr

EDIT2:需求说明:我有多个阅读器,并且可能有多个重新加载器(尽管这种情况不太常见)。读者需要持有一个配置对象,并且在阅读时它不会改变。旧的配置对象必须在最后一个读取器完成后释放。

【问题讨论】:

你能详细说明用例吗?如果您有很多读者,并且一个线程决定重新加载,那么 1)您需要一个信号量,而不是互斥量 2)其他线程应该何时开始读取新值?直到他们应该阅读旧版本时才明确定义。 @kabanus 编辑澄清。另外,如果可能的话,我想避免添加互斥体/信号量。 您正在准确地描述读写信号量。读取操作是免费的,只有在信号量明确的情况下才能进行写入。你用计数器实现的任何东西都是这样的。假设指针分配不是原子的(它可能与 std::atomic 一起),您将不得不创建一个 r/w 信号量。我可以给你看其中一个,否则我没有解决办法。 我想使用 boost 的 shared_mutex,但读取权限不是免费的。总是有一个锁来测试作家的状态。但无论如何,如果可能的话,我正在寻找无锁解决方案。 最后一条评论,为了避免讨论——可以编写一个信号量类/共享互斥类,它不会阻塞读取,但会阻塞写入。编辑: boost shared_mutex 就是这样:boost.org/doc/libs/1_39_0/doc/html/thread/… 【参考方案1】:

你应该更新你的编译器来获得原子共享指针操作。

如果失败,请将其包装在 shared_timed_mutex 中。然后测试一下你花了多少钱。

与正确编写自己的无锁共享指针系统相比,这两者的工作量要少。

如果你必须:


这是一个 hack,但它可能会起作用。它是指针本身的读取-复制-更新样式。

有一个std::vector&lt;std::unique_ptr&lt;std::shared_ptr&lt;T&gt;&gt;&gt;。有一个std::atomic&lt;std::shared_ptr&lt;T&gt; const*&gt;“当前”指针和一个std::atomic&lt;std::size_t&gt; active_readers

vector 存储您还活着的shared_ptrs。当你想改变时,在后面推一个新的。保留此shared_ptr 的副本。

现在将“当前”指针换成新的。忙——等到active_readers 达到零,或者直到你感到无聊。

如果active_readers 命中零,则将vector 过滤为使用计数为1 的shared_ptrs。将它们从vector 中删除。

无论如何,现在将额外的shared_ptr 删除到您创建的状态。写完了。

如果您需要多个写入器,请使用单独的互斥锁锁定此进程。

在阅读器端,增加active_readers。现在以原子方式加载“当前”指针,制作指向shared_ptr 的本地副本,然后递减active_readers

但是,我只是写了这个。所以它可能包含错误。证明并发设计正确是困难

到目前为止,最简单的方法是升级编译器并在 shared_ptr 上进行原子操作。


这可能过于复杂,我认为我们可以设置它以便在最后一个读者离开时清理T,但我的目标是正确性而不是回收T 的效率。


通过阅读器上的最小同步,您可以使用条件变量来表示阅读器已完成给定的T;但这涉及到与作者线程的一点点争用。


实际上,无锁算法通常比基于锁的算法要慢,因为互斥体的开销并不像您担心的那么高。

一个 shared_timed_mutex 包装一个 shared_ptr,作者只是覆盖了变量,这将非常快。现有读者将继续使用他们的旧shared_ptr

【讨论】:

以上是关于无锁重载和共享 const 对象的主要内容,如果未能解决你的问题,请参考以下文章

为 const 引用和 rvalue 引用编写重载

在非常量对象上,为啥 C++ 不调用具有 public-const 和 private-nonconst 重载的方法的 const 版本?

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++之类和对象