多个快速阅读器单个慢速写入器:使用具有原子索引的阴影数据安全吗?

时间:2018-03-23 10:12:29

标签: c++ multithreading c++11 atomic

我不确定我是否可以在标题中正确地概括这个概念,但这就是我的意思:

假设我有"慢",在单个线程对数据结构进行的更新非常不频繁的意义上,而有多个线程连续读取相同的数据结构。

为了避免锁定,并且被卡在C ++ 14上(所以没有std :: shared_mutex可用)并且没有提升,我想到了一种方法,我保留了2个结构副本并使用原子整数来索引当前的一个。

让我们假设2的深度已经足够,让我们不用担心,更新是如此罕见,以至于有足够的时间让新版本的数据结构成为& #34;看出"在新的更新进入之前,所有读者都会这样做。

这是一个片段,显示了我正在做的简化版本:

 /*
 * includes...
 */

struct datastructure_t {
    /*...*/
};


class StructSwapper
{
    std::atomic<unsigned int> current_index_;

    datastructure_t structures_[2];

public:
    StructSwapper (datastructure_t s)
        : current_index_(0)
        , structures_{std::move(s), {}}
    {}

    //Guaranteed to be called _infrequently_ by the same single thread
    void update (datastructure_t newdata)
    {
        auto const next_index = !current_index_.load();

        structures_[next_index] = std::move(newdata);

        current_index_.store(next_index);
    }


    //Called _frequently_ by multiple threads
    datastructure_t const & current_data() const
    {
        return structures_[current_index_.load()];
    }
};

所以基本上当作者线程执行更新时,它首先修改&#34; shadow&#34;数据结构的副本,然后以原子方式更新索引到它的点。

每个读者线程都会执行以下操作:

void reader_thread(StructSwapper const &sw)
{
    auto const &current_data  = sw.current_data();


    if (current_data->find(...))                        //1
    {
        do_something (current_data->val1);              //2

        if (current_data->property2)                    //3
            do_something_else (current_data->val2);     //4
        /*...*/

    }
}

然后我开始思考:什么保证编译器不会在标记为1,2,3,4的任何行中重新读取current_data的值,然后可能会在其中获得两个不同版本的如果在此期间写入线程执行了更新,则执行此函数?

也许如果StructSwapper :: current_data()是内联的,它可能会查看它并看到使用原子变量作为索引,但我怀疑它是否足够。

所以,有两个问题:

  1. 我是否正确地认为这种方法不能保证工作,因为编译器不知道&#34;快照&#34; current_data必须只被用一次吗?
  2. 我认为如果我将数据结构的当前版本的原子引用返回,可能会有所不同,因为在这种情况下,编译器会理解它可以从两个不同的读取中获得两个不同的值,对吧?
  3. 编辑:在看到与更轻松的内存排序相关的优化建议后,我想添加我没有在上面的代码段中报告它们,但它们确实已经在实际代码中已经存在

2 个答案:

答案 0 :(得分:2)

这是一种常见的方法,并且有效。

您需要确保没有其他线程仍在读取您的编写器更新的旧数据。为了解决这个问题,读者通常只需复制数据并在必要时使用它。

  

但后来我开始思考:保证编译器不会重新读取current_data的值。

编译器可能会将变量sw.current_data()的值存储在堆栈中并稍后重新读取,但它不会为您调用void update (datastructure_t newdata) { auto const next_index = !current_index_.load(std::memory_order_relaxed); structures_[next_index] = std::move(newdata); current_index_.store(next_index, std::memory_order_release); // (1) } //Called _frequently_ by multiple threads datastructure_t const & current_data() const { return structures_[current_index_.load(std::memory_order_acquire)]; // Synchronises with (1). }

  

我是否正确地认为这种方法不能保证工作,因为编译器不知道current_data的“快照”必须真正只采用一次?

这是不正确的。

  

我认为如果我将数据结构的当前版本返回原子引用可能会有所不同,因为在这种情况下,编译器会理解它可以从两个不同的读取中获得两个不同的值,对吧?

这是不必要的。

一些优化:

{{1}}

答案 1 :(得分:0)

如果您在任何阅读器仍处理原始数据时进行了第二次更新,您将拥有数据争用,从而产生未定义的行为。

所以,不经常是不够的,你需要保证任何两个更新都足够远,读者将在你获得第二次更新之前使用最新的数据。

通过提供更多可用位置,通过跟踪每个副本使用多少读取器并推迟更新,或者根据需要动态分配多个副本并使用共享指针管理它们,可以减轻这种影响。无论你做什么,都要仔细研究它是否正确和有效。