vendredi 23 mars 2018

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

I'm not sure I could squeeze the concept correctly in the title, but here's what I mean:

Suppose I have "slow", in the sense of really unfrequent, updates made to a data structure by a single thread, while there are multiple threads continuously reading the same data structure.

In the attempt to avoid locks, and being stuck on C++14 (so no std::shared_mutex available) and without boost, I thought to an approach where I keep 2 copies of the structure and use an atomic integer to index the current one.

Let's assume that the depth of 2 is enough and let's not worry about that, updates are so infrequent that there's enough time for a new version of the data structure to be "seen" by all the readers before a new update comes in.

Here's a snippet that shows a simplified version of what I was doing:

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

So basically when the writer thread performs an update, it first modifies the "shadow" copy of the data structure, and then atomically updates the index the points to it.

Each reader thread will do something like:

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
        /*...*/

    }
}

But then I started thinking: what guarantees that the compiler won't re-read the value of current_data in any of the lines marked 1,2,3,4 and then possibly get two different versions of it during the execution of this function if in the meantime an update was performed by the writer thread?

Maybe if the StructSwapper::current_data() is inline, it might have a look at it and see the use of the atomic variable as an index, but I doubt it would be enough anyway.

So, two questions:

  1. Am I right thinking that this approach is not guaranteed to work as the compiler has no clue that the "snapshot" of the current_data has to really be taken only once?
  2. I think it would probably make a difference if I instead returned an atomic reference to the current version of the data structure because in that case, the compiler would understand that it could get two different values out of two different reads, right?

Aucun commentaire:

Enregistrer un commentaire