线程安全的循环缓冲区实现

36

Boost库中的circular_buffer不是线程安全的。因此,我将boost::circular_buffer对象封装在下面所示的类中。使用条件变量、互斥锁和锁的获取/释放来实现线程之间的互斥(我认为)。这个实现是否线程安全?

#include <boost/thread/condition.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/circular_buffer.hpp>

// Thread safe circular buffer 
template <typename T>
class circ_buffer : private boost::noncopyable
{
public:
    typedef boost::mutex::scoped_lock lock;
    circ_buffer() {}
    circ_buffer(int n) {cb.set_capacity(n);}
    void send (T imdata) {
        lock lk(monitor);
        cb.push_back(imdata);
        buffer_not_empty.notify_one();
    }
    T receive() {
        lock lk(monitor);
        while (cb.empty())
            buffer_not_empty.wait(lk);
        T imdata = cb.front();
        cb.pop_front();
        return imdata;
    }
    void clear() {
        lock lk(monitor);
        cb.clear();
    }
    int size() {
        lock lk(monitor);
        return cb.size();
    }
    void set_capacity(int capacity) {
        lock lk(monitor);
        cb.set_capacity(capacity);
    }
private:
    boost::condition buffer_not_empty;
    boost::mutex monitor;
    boost::circular_buffer<T> cb;
};

编辑 现在这是一个模板类,它接受任何类型的对象(而不仅仅是cv::Mat对象)。


4
这似乎更适合在CodeReview上进行讨论。 - Jerry Coffin
1
请原谅我的愚蠢问题,但是在哪些情况下需要使用线程安全的循环缓冲区?在我曾经使用循环缓冲区的所有点上,如果像这样从多个线程访问它,那将是一个严重错误。所以只是出于好奇,你的用例是什么? - LiKao
1
@LiKao 我使用它将网络摄像机的帧捕获到MATLAB中,请参阅我的先前帖子https://dev59.com/mWDVa4cB1Zd3GeqPh-6Z。你会如何处理这个问题? - Alexey
6
你可以使用这样的东西来实现“生产者-消费者队列”(http://en.wikipedia.org/wiki/Producer-consumer_problem)。这种队列可以在“多线程管道”的各个阶段之间使用。 - Emile Cormier
@Alex,如果不麻烦的话,能否提供一个简单的示例,说明如何使用这个类来抓取和显示OpenCV帧? - ttsesm
显示剩余3条评论
6个回答

18

是的。
如果您使用相同锁来锁定所有公共方法,则可以实现线程安全。

您可以考虑使用读写锁,如果您有很多并发读者,则可能具有更好的性能。

如果您没有很多读者,则只会增加开销,但可能值得检查该选项并进行测试。


15
我认为在循环缓冲区中使用读写锁没有意义。生产者和消费者都会修改缓冲区,因此他们实际上都是“写入者”。 - David Rodríguez - dribeas
1
@DavidRodríguez-dribeas - 在这种情况下,你是正确的。我并没有真正涉及设计,只是线程安全部分。 - Yochai Timmer

5

我认为它看起来不错,除了在send中有一些无意义的Mat副本。你不需要新的,可以直接将send的参数推送到你的cb中。


避免无意义、昂贵和增加争议的复制,这是一个好习惯。 - Martin James
@MartinJames我无法直接推送send的参数。 “cv :: Mat类实现了引用计数和浅复制,使得当将图像分配给另一个图像时,图像数据不会被复制,并且两个图像将指向同一内存块。” “保留引用计数,以便仅当对图像的所有引用被销毁时才释放内存。 如果要创建包含原始图像的新副本的图像,则将使用方法copyTo()。”-来自“OpenCV 2计算机视觉应用程序编程食谱”(第28页)。 - Alexey
1
在这种情况下,您仍然不需要使用new,在堆栈上分配新图像同样有效。但是,您难道不想要这个功能吗?即在循环缓冲区中共享副本? - Willem Hengeveld
+1 让我意识到我不需要在堆上分配 Mat。Mat 对象可以存储在堆栈中,例如 Mat image2; image.copyTo(image2); 然后 cb.push_back(image2);。我仍然需要使用 copyTo(),否则缓冲区中的所有对象都将引用最后一个发送的对象。或者,我可以将图像复制移动到 circ_buffer 类之外,这样,是的,我可以像您建议的那样直接将 send 的参数推送到缓冲区。 - Alexey
1
当一个图像被分配给另一个图像时,图像数据不会被复制,两个图像将指向同一内存块。这难道不是你想要的吗?当你将引用分配给队列时,不会进行复制,现在有两个引用。当在生产者中创建新图像并覆盖旧图像指针时,那么只有一个引用 - 在队列上。因此,不需要复制。 - Martin James

2
您的实现与这个blogger所展示的类似。您应该阅读该博客,以查看您的实现中是否有遗漏的部分。
如果您的Mat对象创建/复制起来很昂贵,您应该避免不断地创建/复制/删除它们。相反,您应该有一个池(也称空闲列表)的Mat对象,这些对象在某种管道架构中不断被回收利用。我在这个answer中描述了这种类型的架构。
在那个答案中,我建议使用阻塞栈来实现池,但是您也可以使用阻塞的circular_buffer。我之所以建议使用栈,是因为我认为它可能更加缓存友好,但我从未实际测量过它是否会有差异。

Mat对象(图像)不太大,在每次调用时大小基本相同。我刚意识到我可以在堆栈上分配Mat对象。 - Alexey
我已经使用基于阻塞队列的对象池很长时间了。实际上,我几乎只在我的多线程应用程序设计中使用poolObject+消息传递模式。无复制、无malloc、无GC和内置流量控制不是唯一的优点。在GUI应用程序中,在任何表单或工作线程之前创建对象池意味着我通常可以忘记显式池销毁或线程终止的麻烦。定时器上的池级别转储显示出泄漏,而不需要像V*******这样的令整个应用程序变得缓慢的可怜的第三方泄漏工具。 - Martin James
默认情况下,cv::Mat的复制构造函数创建浅拷贝。 - Reunanen

1

非常古老的问题 :) 这里有一个具有无锁实现的 dis gin 链接

这里有一个 BSD-2 许可证的库 链接


0

乍一看看起来不错,但是你根本没有使用buffer_not_full条件。你可能想要添加类似于buffer_not_empty代码的代码。


1
如果数据源产生的数据超过了缓冲区的容量,boost::circular_buffer对象会覆盖最旧的数据。这是可以接受的。因此,不需要检查“buffer_not_full”条件。 - Alexey

0
//This implementation above is broken. You also need condition variable
//boost::condition buffer_not_full; and wait in send on available space in the circular buffer.
enter code here

void send (T imdata) {
   lock lk(monitor);
   while (cb.full())
      buffer_not_full.wait(lk);
   cb.push_back(imdata);
   buffer_not_empty.notify_one();
}

T receive() {
     lock lk(monitor);
     while (cb.empty())
        buffer_not_empty.wait(lk);
     T imdata = cb.front();
     cb.pop_front();
     buffer_not_full.notify_one();
     return imdata;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接