3

我正在开发一个图像处理应用程序,我的主线程上有两个线程:

1 - CameraThread从网络摄像头捕获图像并将它们写入缓冲区

2 - ImageProcessingThread,从该缓冲区获取最新图像进行过滤。

这是多线程的原因是因为速度至关重要,我需要让 CameraThread 继续抓取图片并让 ImageProcessingThread 准备好最新的捕获,同时它仍在处理前一个图像。

我的问题是找到一种快速且线程安全的方式来访问该公共缓冲区,我认为理想情况下,它应该是一个三重缓冲区(图像 [3]),这样如果 ImageProcessingThread 很慢,那么 CameraThread 可以继续在另外两个图像上书写,反之亦然。

哪种锁定机制最适合线程安全?

我查看了 lock 语句,但似乎它会使线程阻塞等待另一个线程完成,这与三重缓冲的观点相悖。

提前感谢您的任何想法或建议。

J。

4

7 回答 7

8

这可能是生产者-消费者模式的教科书示例。

如果您打算在 .NET 4 中工作,您可以使用IProducerConsumerCollection<T>和关联的具体类来提供您的功能。

如果没有,请阅读本文以获取有关该模式的更多信息,并阅读此问题以获取编写您自己的阻塞先进先出结构的线程安全实现的指导。

于 2010-03-25T15:31:30.980 回答
3

就个人而言,我认为您可能希望为此考虑一种不同的方法,而不是写入您必须管理访问的集中式“缓冲区”,您是否可以切换到使用事件的方法。一旦相机线程“接收到”图像,它就可以引发一个事件,将图像数据传递给实际处理图像处理的进程。

另一种方法是使用队列,该队列是 FIFO(先进先出)数据结构,现在它不是线程安全的访问,因此您必须锁定它,但您的锁定时间将非常短将项目放入队列中。您还可以使用其他线程安全的 Queue 类。

使用您的方法,您将不得不应对许多问题。访问数组时阻塞,可用数组插槽用完后发生的限制,阻塞等。

于 2010-03-25T15:25:46.547 回答
2

考虑到图片所需的进动量,我认为简单的锁定方案不会成为您的瓶颈。在你开始在错误的问题上浪费时间之前进行测量。
使用“无锁”解决方案要非常小心,它们总是比看起来更复杂。

你需要一个队列,而不是一个数组。
如果你可以使用 dotNET4,我会使用 ConcurrentQuue。

于 2010-03-25T15:37:25.413 回答
1

您将不得不运行一些性能指标,但请查看lock free queues.

例如,请参阅此问题及其相关答案

但是,在您的特定应用程序中,您的处理器只对最近的图像真正感兴趣。实际上,这意味着您真的只想维护两个项目(新项目和前一个项目)的队列,以便在读取和写入之间不存在争用。例如,您可以让您的生产者在写入新条目后从队列中删除旧条目。

编辑:说了这么多,我认为米切尔·塞勒斯的回答中有很多优点。

于 2010-03-25T15:24:49.107 回答
0

我会考虑使用ReaderWriterLockSlim,它允许快速读取和可升级的写入锁。

于 2010-03-25T15:27:32.703 回答
0

这不是您问题的直接答案,但重新考虑您的并发模型可能会更好。锁是同步任何东西的糟糕方法——级别太低、容易出错等。尝试在消息传递并发方面重新考虑您的问题:

这里的想法是每个线程都是它自己的紧密包含的消息循环,每个线程都有一个用于发送和接收消息的“邮箱”——我们将使用 MailboxThread 一词来区分这些类型的对象和普通的简线程。

因此,不是让两个线程访问同一个缓冲区,而是让两个 MailboxThreads 在彼此之间发送和接收消息(伪代码):

let filter =
    while true
        let image = getNextMsg() // blocks until the next message is recieved
        process image

let camera(filterMailbox) =
    while true
        let image = takePicture()
        filterMailbox.SendMsg(image) // sends a message asyncronous

let filterMailbox = Mailbox.Start(filter)
let cameraMailbox = Mailbox.Start(camera(filterMailbox))

现在您正在处理的线程根本不知道或不关心任何缓冲区。他们只是等待消息并在消息可用时对其进行处理。如果您发送许多消息以供 filterMailbox 处理,则这些消息将排队等待稍后处理。

这里的难点实际上是实现您的 MailboxThread 对象。虽然它需要一些创造力才能做到正确,但完全有可能实现这些类型的对象,以便它们仅在处理消息时保持线程打开,并在没有消息要处理时将正在执行的线程释放回线程池(此实现允许您在没有悬空线程的情况下终止应用程序)。

这里的优点是线程如何发送和接收消息而不用担心锁定或同步。在幕后,您需要在消息入队或出队之间锁定消息队列,但该实现细节对您的客户端代码是完全透明的。

于 2010-03-25T15:43:49.957 回答
0

只是一个想法。

由于我们只讨论两个线程,我们可以做出一些假设。

让我们使用您的三重缓冲想法。假设只有 1 个写线程和 1 个读线程,我们可以以整数的形式来回扔一个“标志”。两个线程都将持续旋转,但会更新它们的缓冲区。警告:这仅适用于1 个读者线程

伪代码

共享变量:

整数状态 = 0;//0 = 准备好写入;1 = 准备阅读

Buffer1 = 新字节[]

Buffer2 = 新字节[]

Buffer3 = 新字节[]

BufferTmp = null

线程1 {

while(true)
{
    WriteData(Buffer1);
    if (Status == 0)
    {
        BufferTmp = Buffer1;
        Buffer1 = Buffer2;
        Buffer2 = BufferTmp;
        Status = 1;
    }
}

}

线程2 {

while(true)
{
    ReadData(Buffer3);
    if (Status == 1)
    {
        BufferTmp = Buffer1;
        Buffer2 = Buffer3;
        Buffer3 = BufferTmp;
        Status = 0;
    }
}

}

请记住,您的 writedata 方法不会创建新的字节对象,而是更新当前的。创建新对象的成本很高。

此外,您可能希望 ELSE 语句中的 thread.sleep(1) 伴随 IF 语句,否则一个单核 CPU、一个旋转线程会在另一个线程被调度之前增加延迟。例如。写入线程可能会在读取线程被调度之前运行 2-3 次,因为调度程序看到写入线程正在“工作”

于 2010-03-26T18:23:42.613 回答