环形缓冲区,1位作者和N位读者

时间:2015-01-29 10:53:28

标签: multithreading algorithm c++11 design-patterns concurrency

我需要一种环形缓冲区(或者一些更合适的数据结构,以防万一)和一种用于在以下情况下处理环形缓冲区的算法/模式。

1个连续生成实时数据的编写器必须始终能够写入第一个空闲“插槽”(一个不在读取过程中的插槽),或者等到一个插槽可以自由写入。每当作者完成将数据写入一个插槽时,它就可以为读者“提交”该插槽。

在给定时间,可能有N个并发读者。在读取请求发生时,每个读取器应始终从环形缓冲区中最后一个提交的插槽中获取最新的写入数据,但不得多次读取相同的数据。如果编写者自上次读取以来没有在一个插槽中写入和提交新数据,那么读者应该等待(想想快速阅读器)。

请注意,1个阅读器不得“使用”其他阅读器的数据。换句话说,两个不同的读者可以读取相同的数据。同样,一个读者可以从同一个插槽中读取数据两次或多次,但前提是写入者在两个读取请求之间写入该插槽。

请注意,破坏者可能不适合我的情况(或者我没有按照我的意愿使其工作)。破坏者的问题在于作者可能会如此迅速地进步(与较慢的读者相比),它可能会在他们正在阅读的过程中覆盖一些插槽。在这种情况下,编写者应该能够跳过这些“忙”插槽,直到它找到第一个空闲插槽(一旦写入,它还必须发布该插槽),但是破坏者模式不会似乎在考虑这种情况。序列本身还有另一个问题,在我使用的破坏器实现中,它是一个原子整数,所以它可能会溢出导致一些未定义的行为。

你知道吗?如果你知道的话,我会很欣赏现代C ++中的解决方案。

2 个答案:

答案 0 :(得分:0)

我不知道这个问题的开箱即用解决方案(注意:这并不意味着没有一个!)但如果我正在尝试使用c ++ 11设施我正在研究以下内容:

  • 用于保存数据的数组
  • 用于保存已编写元素的索引的std :: stack
  • 用于保存已读取元素的索引的std :: queue
  • 用于控制对堆栈的访问的互斥锁。因此,读者和作者在弹出/推入堆栈之前就会抓住互斥锁。
  • 用于控制对队列的访问的互斥锁。因此,读者和作者在将Mutex排队/出列到队列之前就会抓住它。
  • 读取器条件变量,供读者在堆栈为空时阻止
  • 编写器条件变量,供队员在队列为空时阻止。

  • 开始将所有索引(环形缓冲区的大小)排入队列。

  • 编写器获取索引并写入数组,然后将索引压入堆栈并通知读者。
  • 读者从堆栈中弹出索引,并在完成后将它们排入队列并通知作者。
  • 空堆栈导致读者等待编写器,空队列导致编写器等待读者。

A nice article on the C++11 concurrency features.

编辑进一步想到这个建议尝试做的是将数组中的插槽视为资源池。读者获取资源,即插槽的索引,并在完成处理后将其返回。

更新解决方案:由于插槽数量限制为最多8个,我建议您尽可能简化解决方案。让每个插槽负责自己的状态:

#include <atomic>
template <typename T>
class Slot {
   atomic_uint readers ;
   int iteration ;
   T data ;
}

然后创建一个std:Slots向量,让读者和作者遍历向量。

作者需要找到readers == 0(可能最小值为iteration)的插槽,当它需要时,需要减少读取器数量,然后检查读取器数量是 - 1确保读者没有开始读取if和the decrement之间的内容。如果确实如此,则重新增加readers并重新开始。

读者遍历数组,寻找他们尚未读取的最大iteration值以及readers >= 0。然后他们重新检查readers的值,如果Writer没有将它设置为-1,则递增它并开始读取。如果Writer已经开始编写该插槽,那么Reader将重新开始。

让我们说Writer已经确定一个插槽是免费的,并且Reader已经决定它需要读取插槽,有两个posibile命令,并且在Reader和Writer中都要退回并重试。

Execution       Writer                  Reader
1               readers == 0 
2                                       readers >= 0
3               readers--
4                                       readers++
5               readers == -1 is false
6                                       readers > 0 is false!
7               readers++
8                                       readers--


Execution       Writer                  Reader
1                                       readers >= 0
2               readers == 0 
3                                       readers++
4               readers--
5                                       
6               readers == -1 is false
7                                       readers > 0 is false
8               readers++
9                                       readers--

如果Reader和Writer以这种方式发生冲突,则无法访问,并且都需要重试。

如果您不熟悉这种方法,那么可以使用Mutex和一个lock / try_lock,在Slot类中就是这样的(NB:目前不能通过编译器运行它,因此可能会出现语法错误)

typedef enum LOCK_TYPE {READ, WRITE} ;
std::mutex mtx ;

bool lockSlot(LOCK_TYPE lockType)
{
    bool result = false ;
    if (mtx.try_lock()) {

        if ((lockType == READ) && (readers >= 0)) 
            readers++ ;
        if ((lockType == WRITE) && (readers == 0)) 
            readers-- ;
        result = true ;
    }
    return result ;
}

void unlockSlot (LOCK_TYPE lockType)
{
    if (mtx.lock()) {  // Wait for a lock this time

        if (lockType == READ) 
            readers-- ;
        if (lockType == WRITE)
            readers++ ;
    }
}

当/如果Reader失去工作,它可以等待一个条件变量,当新数据可用时,Writer可以使用NotifyAll()读取器。

答案 1 :(得分:0)

几年前我遇到了同样的问题(如果我理解正确的话)并且我有一个解决方案。我接受了一份带有算法的期刊论文,我会在这里发表一篇参考文献,但是现在,我可以引导你去编写代码。

首先,这是在C中实时完成的,并且(当前)可能使用POSIX后端(在用户空间中)或RTAI后端(在用户或内核空间中)。 / p>

其次,我的要求是作者因用户而永远被阻止。也就是说,如果编写器找不到空闲缓冲区,它会覆盖当前数据帧。我也解决了这个问题,我的测试显示,只需在阅读器部分进行小型协作,就可以完全避免这种情况。

另请注意,该想法类似于&#34;周期性数据缓冲区&#34; 1 和&#34;循环异步缓冲区&#34; 2 ,尽管我自己想出来了。他们使用原子操作,而我使用读写器锁。此外,他们没有提供交换跳过的解决方案&#34;正如我上面提到的那样。

最后,作家和读者不仅仅是周期性的。作者可以是定期的或零星的(根据请求运行),读者可以是定期的,零星的或尽力而为。

您可以找到编写器实现here和读者实现here

为了完整起见,我将使用基本算法粘贴注释,以便在多缓冲模式下定期编写和读取器:

Writer:

write_lock(cur)
loop {
         if last period no swap
                 try swap again
         write
         try while period left
                 write_lock(next)
                 write_unlock(cur)
                 cur = next
         wait period
}
unlock(cur)

Reader:

loop {
        if last is new
                b = last
        else
                b = cur
        read_lock(b)
        read
        read_unlock(b)
        wait period
}

write上,编写器还为缓冲区中的数据加时间戳。这对于读者了解数据是新的还是旧的非常有用。

有关写入器中缓冲区交换的要点是它尝试,直到成功或接近其周期结束时才交换缓冲区。这是通过try_lock所有其他缓冲区来完成的,以查看哪些可用(在第一个成功/结束时间停止)(当然没有繁忙的循环)。一旦完成,它还会告诉(通过共享内存,可能与包含数据缓冲区的内存相同),何时需要再次交换缓冲区,以及哪个缓冲区包含最新数据(last) ,以及作者当前控制的缓冲区(cur)。

然后读者可以选择。如果last包含新数据read_lock,则继续阅读。如果没有,或者如果读者可以估计它无法执行下一次缓冲交换的读取时间,则它会等待cur解锁。然后,一旦作者解锁,实时调度程序就会尽快唤醒读者。如果系统没有处于高负荷状态,这几乎意味着即时。

为了让读者知道它是否能够及时执行读取,它会跟踪它在周期中的平均执行时间,并使用它来检查是否current_time + my_execution_time > next_expected_swap。在这种情况下,它无法及时执行读取操作,因此它会在cur上等待,这很快就会被解锁。


1 &#34; HIC:伺服回路层次结构的操作系统&#34;,Clark,D.,Robotics and Automation,1989。Proceedings。,1989 IEEE International Conference on

2 &#34; HARTIK:机器人应用的实时内核&#34;,Buttazzo,G.C。,Real-Time Systems Symposium,1993。,Proceedings。


注意:虽然此库主要是为机器人皮肤数据处理而设计的,但您实际上可以将它用于具有上述属性的任何一般作者 - 读者通信。此外,请注意我指向的存储库的主分支包含旧版本。我指出的分支skinware-2具有更好,功能更丰富的实现。