繁体   English   中英

多个快速读取器和一个慢速写入器:使用具有原子索引的影子数据是否安全?

[英]Multiple fast readers single slow writer: is using shadow-data with atomic index safe?

我不确定标题中的概念是否正确,但这是我的意思:

假设我的速度很慢,实际上是由单个线程对数据结构进行的更新“缓慢”,而有多个线程连续读取同一数据结构。

为了避免锁定,并卡在C ++ 14上(因此没有可用的std :: shared_mutex)并且没有增强功能,我想到了一种方法,其中保留结构的2个副本并使用原子整数来索引当前一。

假设2的深度就足够了,不用担心,更新是如此罕见,以至于有足够的时间让新版本的数据结构在新更新出现之前被所有读者“看到”。

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

 /*
 * 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()];
    }
};

因此,基本上,当编写器线程执行更新时,它首先修改数据结构的“影子”副本,然后原子地更新指向它的索引。

每个阅读器线程都将执行以下操作:

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. 我是否正确地认为,由于编译器不知道current_data的“快照”实际上只需要执行一次,因此不能保证这种方法有效吗?
  2. 我认为,如果我改为返回对数据结构当前版本的原子引用,那可能会有所不同,因为在这种情况下,编译器会理解,它可以从两次不同的读取中获得两个不同的值,对吗?

编辑:在看到与更宽松的内存顺序相关的优化建议之后,我想补充一点,我没有在上述代码段中报告它们,但实际上它们已经存在于实际代码中。

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

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

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

编译器可能会将变量current_data的值存储在堆栈中,并在以后重新读取,但不会为您调用sw.current_data()

我是否正确地认为,由于编译器不知道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).
}

如果在任何读取器仍在处理原始数据的同时进行了第二次更新,则将有数据争用,因此也将具有未定义的行为。

因此,不频繁是不够的,您需要确保任何两个更新之间的距离都足够远,以使读者在获得第二个更新之前都将使用最新数据。

可以通过以下方法来缓解这种情况:增加可用的位置,跟踪每个副本有多少读者,并推迟更新,或者动态分配所需数量的副本并使用共享指针进行管理。 无论您做什么,都要认真看一下它是否正确和有效。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM