c++multithreadingatomicseqlock

Atomic and lock-free write of arbitrary sized data/vector/array


I am trying to implement the following functionality:

Usecase: I am trying to write the software for a CNC machine. The step pulses for the motors are generated in software in a realtime thread. This realtime thread is constantly updating a variable which holds the current position of the axis. Multiple other non-realtime threads may read that variable, e.g. to display the current position.

Question 1: Is there a standard/accepted solution or pattern for this kind of problem?

I came up with the following idea: use an std::atomic<uint64_t> to protect the data and track weather a thread is currently writing (by checking the last bit) or has written since the read started (by incrementing the value on a write).

template <class DATA, class FN>
void read_modify_write(DATA& data, std::atomic<uint64_t>& protector, FN fn)
{
  auto old_protector_value = protector.load();
  do
  {
    // wait until no other thread is writing
    while(old_protector_value % 2 != 0)
      old_protector_value = protector.load();

    // try to acquire write privileges
  } while(!protector.compare_exchange_weak(old_protector_value, old_protector_value + 1));

  // write data
  data = fn(data);

  // unlock
  protector = old_protector_value + 2;
};

template <class DATA>
DATA read(const DATA& data, std::atomic<uint64_t>& protector)
{
  while(true)
  {
    uint64_t old_protector_value = protector.load();

    // wait until no thread is writing
    while(old_protector_value % 2 != 0)
      old_protector_value = protector.load();

    // read data
    auto ret = data;

    // check if data has changed in the meantime
    if(old_protector_value == protector)
      return ret;
  }
}

Question 2: Is the above code thread-safe and fulfilling above requirements?

Question 3: Can it be improved?

(The only theoretical problem I could find is if the counter wraps around, i.e. exactly 2^63 write operations are performed during 1 read operation. I would consider this weakness acceptable if there are no better solutions.)

Thank you


Solution

  • Strictly speaking your code is not lock-free, because you effectively use the LSB of protector to implement a spinlock.

    Your solution looks very much like a sequence lock. However, the actual read operation auto ret = data; is strictly speaking a data race. To be fair, it is simply not possible to write a fully standard compliant seqlock in C++17, for that we have to wait for C++20.

    It is possible to extend the seqlock to make read operations lock-free at the cost of higher memory usage. The idea is to have multiple instances of the data (let's call them slots), and the write operation always writes to the next slot in a round robin fashion. This allows a read operation to read from the last fully written slot. Dmitry Vyukov described his approach in Improved Lockfree SeqLock. You can take a look at my seqlock implementation that is part of my xenium library. It also optionally allows lock-free read operations with a configurable number of slots (though it slightly differs to Vyukov in how a reader finds the last fully written slot).