多线程双缓冲区

Posted

技术标签:

【中文标题】多线程双缓冲区【英文标题】:Multithreaded double buffer 【发布时间】:2020-02-22 11:04:31 【问题描述】:

我有一个Buffer 类,它实现了在设计时考虑到多线程的double buffer pattern:

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;
;

这个类包含两个缓冲区。缓冲区总是通过指针访问:

读取的缓冲区,由current 指针指定。 写入的缓冲区,由next 指针指定。

将有两个线程:一个更新线程将调用write 方法写入下一个缓冲区,一个读取线程将调用read 方法从当前缓冲区读取。当更新线程完成后,它会调用swap 来交换指向缓冲区的指针。

缓冲区的交换必须以原子方式完成,因此我必须在每个方法中锁定互斥锁(创建 lk 对象)。

问题是在每个方法中锁定互斥锁会阻止两个线程同时访问它们对应的缓冲区。但是这两个缓冲区是独立的:如果一个线程修改一个缓冲区,而另一个线程读取另一个缓冲区,则没有问题。

我想允许更新线程在读取线程读取其相应缓冲区的同时修改其缓冲区。有什么方法可以实现吗?

【问题讨论】:

可能有两个互斥锁:一个只用于读取,另一个用于写入。 swap() 必须锁定这两个互斥锁。 谁在你的代码中调用swap()?读者还是作者?虽然你有锁可以使交换原子和读写两个缓冲区的单个元素成为原子的,但没有什么可以真正使访问缓冲区作为一个整体原子的。 @G.Sliepen 作者在完成后调用 swap()。 这意味着如果一个读者想要,例如,通过写float diff = buffer.read(1) - buffer.read(0)来获得两个值的差异,那么虽然这两个读是原子的,但写者可以在两者之间调用buffer.swap()两读。最终结果可能不是您所期望的。 【参考方案1】:

解决您的问题的一种方法是每个缓冲区使用一个互斥锁并同时保护您的交换。 如果在两个线程之间以同步方式安全地使用交换,您可以安全地移除内部的锁(但在您的代码中似乎并非如此)。

这是一个例子:

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, lkRead);

      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;
;

【讨论】:

以上是关于多线程双缓冲区的主要内容,如果未能解决你的问题,请参考以下文章

多线程异步日志系统,高效强悍的实现方式-双缓冲

重温一下读写双缓冲问题

Vulkan多线程渲染

Linux多线程编程

多线程面试题系列(16):多线程十大经典案例之一 双线程读写队列数据

利用双缓冲队列来减少锁的竞争