c++multithreadinglocking

Multithreaded double buffer


I have a Buffer class that implements the double buffer pattern designed with multithreading in mind:

class Buffer {
public:
   void write(size_t idx, float value) {
      std::lock_guard<std::mutex> lk(mut);
      (*next)[idx] = value; // write to next buffer
   }

   float read(size_t idx) const {
      std::lock_guard<std::mutex> lk(mut);
      return (*current)[idx]; // read from current buffer
   }

   void swap() noexcept {
      std::lock_guard<std::mutex> lk(mut);
      std::swap(current, next); // swap pointers
   }

   Buffer() : buf0(32), buf1(32), current(&buf0), next(&buf1) { }
private:
   std::vector<float> buf0, buf1; // two buffers
   std::vector<float> *current, *next;
   mutable std::mutex mut;
};

This class contains two buffers. The buffers are always accessed through pointers:

There will be two threads: an updating thread will write to the next buffer calling the write method, a reading thread will read from the current buffer calling the read method. When the updating thread is done it calls swap to swap the pointers to the buffers.

The swapping of the buffers must be done atomically so I have to lock the mutex in every method (creating the lk object).

The problem is that locking the mutex in every method prevents the two threads from accessing their corresponding buffer at the same time. But the two buffers are independent: there is no problem if one thread modifies one buffer while the other thread reads the other buffer.

I want to allow the updating thread to modify its buffer at the same time the reading thread reads its corresponding buffer. Any way to achieve this?


Solution

  • One way to solve your problem is by using one mutex per buffer and protect your swap with both. If the swap is safely used in a synchronous way between the two threads, you can safely remove the lock inside (however it seems to not be the case in your code).

    Here is an example:

    class Buffer {
    public:
       void write(size_t idx, float value) {
          std::lock_guard<std::mutex> lk(mutWrite);
          (*next)[idx] = value; // write to next buffer
       }
    
       float read(size_t idx) const {
          std::lock_guard<std::mutex> lk(mutRead);
          return (*current)[idx]; // read from current buffer
       }
    
       void swap() noexcept {
          // Lock both mutexes safely using a deadlock avoidance algorithm
          std::lock(mutWrite, mutRead);
          std::lock_guard<std::mutex> lkWrite(mutWrite, std::adopt_lock);
          std::lock_guard<std::mutex> lkRead(mutRead, std::adopt_lock);
    
          // In C++17, you can replace the 3 lines above with just the following:
          // std::scoped_lock lk( mutWrite, mutRead );
    
          std::swap(current, next); // swap pointers
       }
    
       Buffer() : buf0(32), buf1(32), current(&buf0), next(&buf1) { }
    private:
       std::vector<float> buf0, buf1; // two buffers
       std::vector<float> *current, *next;
       mutable std::mutex mutRead;
       std::mutex mutWrite;
    };