1
\$\begingroup\$

This is a very simple read-copy-update (RCU)-inspired synchronization class:

#include <atomic>


template<typename T>
class RCU
{
private:

    std::atomic<T*>  curr;
    std::atomic<T*>  prev;
    std::atomic<int> readers;

public:

    RCU()
    : curr   (new T),
      prev   (nullptr),
      readers(0)
    {

    }

    ~RCU()
    {
        delete curr;
        delete prev;
    }

    void lock()
    {
        readers++;
    }

    void unlock()
    {
        readers--;
    }

    T* get()
    {
        return curr.load();
    }

    T* clone()
    {
        return new T(*curr.load());
    }

    bool swap(T* t)
    {
        /* If prev is full, a swap is still pending. Clean up memory and
        abort the operation. */

        if (prev.load() != nullptr)
        {
            delete t;
            return false;
        }

        /* Store the current pointer into prev. Readers might still be reading
        through the pointer. */

        prev.store(curr);

        /* Curr contains the new proposed value. No one is reading from this
        right now. */

        curr.store(t);

        /* Spin until there are no more readers to *prev. When so, free the
        memory it points to and say goodbye. */

        while (readers.load() > 0);

        T* old = prev.load();
        prev.store(nullptr);
        delete old;

        return true;
    }
};

The idea is to have many reader threads that repeatedly read from some shared data, while a writer thread that rarely updates it. The writer thread takes care of cleaning up the memory when no readers are reading. Readers are "tracked" through the lock() and unlock() methods. The RCU-like class should synchronize the threads in a lock-free way.

Usage:

struct Data
{
    int x;
    int y;
    int z;
};


RCU<Data> rcu;


void writer_thread()  // the writer rarely runs
{
    while (true)
    {
        Data* data = rcu.clone(); 
        // modify data as needed
        data->x++; 
        data->y++;
        data->z++;
        rcu.swap(data); // Checking the return value might be useful 
    }
}


void reader_thread() 
{
    while (true)
    {
        rcu.lock();
        std::cout << rcu.get()->x << "\n"; // read from data
        rcu.unlock();
    }
}

Questions: does it make sense? Is it really thread-safe? Can a thread slip through the cracks somewhere, somehow?

\$\endgroup\$
  • \$\begingroup\$ Did you test this? Does it work as expected? \$\endgroup\$ – Mast Jul 17 at 6:23
  • \$\begingroup\$ @Mast I did. I let it run for ~15 minutes without nasty surprises. So far so good. \$\endgroup\$ – Ignorant Jul 18 at 8:15
4
\$\begingroup\$

Serious issues with this:

  1. Memory Management.
  2. Not using RAII to lock/unlock

Memory Management

Don't pass pointers it does not indicate owners. Always wrap pointers in a smart pointer. Have a look at std::unique_ptr.

But for types like Data there is no need to use pointers. Simply use Data as the object type (not Data*).

RAII

Look up the concept of RAII.

A good example is std::lock_guard https://en.cppreference.com/w/cpp/thread/lock_guard This shows you how to use RAII to automate the processes of lock/unlock so it is exception safe.

\$\endgroup\$
  • \$\begingroup\$ Good points! Regarding Data*: I'm using dynamic memory allocation on purpose to support non-primitive, heavy-weight types (vectors, maps, ...). Does it make any sense? \$\endgroup\$ – Ignorant Jul 15 at 20:54
  • 2
    \$\begingroup\$ No because you can allow the person using your class decide if it is heavy weight and allow them to templatize the class specifically with a pointer type RCU<MyHeavyWeightClass*>. \$\endgroup\$ – Martin York Jul 15 at 21:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.