在单写入器多读取器线程中交换缓冲区

时间:2011-10-18 10:41:12

标签: c linux algorithm readerwriterlock double-buffering

故事

有一个写作者线程,定期从某个地方收集数据(实时,但在问题中并不重要)。然后有许多读者从这些数据中读取数据。通常的解决方案是使用两个读写器锁和两个缓冲区,如下所示:

Writer (case 1):
acquire lock 0                        
loop
    write to current buffer
    acquire other lock
    free this lock
    swap buffers
    wait for next period

或者

Writer (case 2):
acquire lock 0                        
loop
    acquire other lock
    free this lock
    swap buffers
    write to current buffer
    wait for next period

问题

在这两种方法中,如果获取其他锁操作失败,则不进行交换,编写器将覆盖其先前的数据(因为编写器是实时的,它不能等待读者)所以在这种情况下,所有读者都会失去这一数据框架。

这不是什么大问题,读者是我自己的代码而且它们很短,所以使用双缓冲区,这个问题就解决了,如果有问题我可以使它成为三重缓冲区(或更多)。

问题是我想要最小化的延迟。想象一下案例1:

writer writes to buffer0                reader is reading buffer1
writer can't acquire lock1              because reader is still reading buffer1
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)      <- **this point**
|
|
writer wakes up, and again writes to buffer0

在这一点**,如果只有作者在读者完成后可以进行交换而不是等待下一个时期,理论上其他读者理论上可以读取buffer0的数据。在这种情况下发生的事情是,仅仅因为一个读者有点迟了,所有读者都错过了一帧数据,而问题可以完全避免。

案例2类似:

writer writes to buffer0                reader is idle
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)
|
|                                       reader starts reading buffer1
writer wakes up                         |
it can't acquire lock0                  because reader is still reading buffer1
overwrites buffer0

我尝试混合解决方案,因此编写器会在写入后立即尝试交换缓冲区,如果不可能,则在下一个周期唤醒之后。所以像这样:

Writer (case 3):
acquire lock 0                        
loop
    if last buffer swap failed
        acquire other lock
        free this lock
        swap buffers
    write to current buffer
    acquire other lock
    free this lock
    swap buffers
    wait for next period

现在延迟的问题仍然存在:

writer writes to buffer0                reader is reading buffer1
writer can't acquire lock1              because reader is still reading buffer1
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)      <- **this point**
|
|
writer wakes up
swaps buffers
writes to buffer1

再次在**这一点**,所有读者都可以开始阅读buffer0,这是buffer0编写后的短暂延迟,但他们必须等到下一个时期作家。

问题

问题是,我该如何处理?如果我希望编写器在所需的时间段内精确执行,则需要使用RTAI函数等待这段时间,我不能像

那样执行
Writer (case 4):
acquire lock 0                        
loop
    write to current buffer
    loop a few times or until the buffer has been swapped
        sleep a little
        acquire other lock
        free this lock
        swap buffers
    wait for next period

这引入了抖动。因为“少数几次”可能会比“等待下一个时期”更长,所以作者可能会错过其时期的开始。

为了更清楚,这就是我想要发生的事情:

writer writes to buffer0                reader is reading buffer1
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)      As soon as all readers finish reading,
|                                         the buffer is swapped
|                                       readers start reading buffer0
writer wakes up                         |
writes to buffer1

我已经找到了什么

我发现read-copy-update据我所知,它为缓冲区分配内存并释放它们直到读者完成它们,这对我来说是不可能的。一,线程在内核和用户空间之间共享。其次,使用RTAI,您无法在实时线程中分配内存(因为那时您的线程将调用Linux的系统调用,从而打破实时性!(更不用说使用Linux自己的RCU实现是无用的)出于同样的原因)

我还想过有一个额外的线程,它以更高的频率尝试交换缓冲区,但这听起来不是一个好主意。首先,它本身需要与作者同步,其次,我有许多这些作家 - 读者并行工作在不同的部分,每个作家的一个额外的线程似乎太多了。对于与每个编写器的同步,所有编写者的一个线程似乎非常复杂。

5 个答案:

答案 0 :(得分:3)

您使用什么API进行读写器锁定?你有一个定时锁,比如pthread_rwlock_timedwrlock吗?如果是,我认为它是您问题的解决方案,如下面的代码所示:

void *buf[2];

void
writer ()
{
  int lock = 0, next = 1;

  write_lock (lock);
  while (1)
    {
      abs_time tm = now() + period;

      fill (buf [lock]);
      if (timed_write_lock (next, tm))
        {
          unlock (lock);
          lock = next;
          next = (next + 1) & 1;
        }
      wait_period (tm);
    }
}


void
reader ()
{
  int lock = 0;
  while (1)
    {
      reade_lock (lock);
      process (buf [lock]);
      unlock (lock);
      lock = (lock + 1) & 1;
    }
}

这里发生的事情是,对于作者来说,无论是等待锁定还是下一个时期都没有关系,只要它确定在下一个时期到来之前就会被唤醒。绝对超时确保了这一点。

答案 1 :(得分:1)

这不是三重缓冲应该解决的问题。所以你有3个缓冲区,让我们称它们为write1,write2和read。写入线程在写入write1和write2之间交替,确保它们永远不会阻塞,并且最后一个完整帧始终可用。然后在读取线程中,在某个适当的位置(例如,在读取帧之前或之后),使用可用的写入缓冲区翻转读取缓冲区。

虽然这可以确保编写器永远不会阻塞(缓冲区翻转可以通过翻转两个指针来快速完成,甚至可能使用CAS原子而不是锁定),仍然存在读者不得不等待其他问题的问题读者在翻转之前完成读缓冲。我想这可以通过一个读取缓冲池来轻微地解决RCU问题,其中可以翻转一个可用的缓冲区。

答案 2 :(得分:1)

  • 使用队列(FIFO链表)
  • 实时编写器将始终追加(入队)到队列的末尾
  • 读者将始终从队列的开头删除(出队)
  • 如果队列为空,读者将阻止

编辑以避免动态分配

我可能会使用循环队列...... 我会使用内置的__sync原子操作。 http://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html#Atomic-Builtins

  • 循环队列(FIFO 2d阵列)
    • ex:byte [] [] Array = new byte [MAX_SIZE] [BUFFER_SIZE];
    • 开始和结束索引指针
  • Writer覆盖Array [End] []
  • 的缓冲区
    • 如果编写器最终以循环方式循环
    • ,则可以递增
  • Reader从Array [Start] []
  • 获取缓冲区
    • 读取器阻止如果开始==结束

答案 3 :(得分:1)

如果您不希望作者等待,也许它不应该获得任何其他人可能持有的锁。不过,我会让它执行某种同步,以确保写出的内容真正被写出来 - 通常,大多数同步调用都会导致内存刷新或屏障指令被执行,但细节将取决于内存模型你的cpu和你的线程包的实现。

我想看看周围是否还有其他同步原语可以更好地适应,但是如果推动推动我会让编写器锁定并解锁一个其他人都没用过的锁。

然后读者必须准备好时不时地错过任何东西,并且必须能够检测到他们什么时候错过了东西。我会将有效性标志和长序列计数与每个缓冲区相关联,并使编写器执行类似“清除有效性标记,增量序列计数,同步,写入缓冲区,增量序列计数,设置有效性标记,同步”的操作。如果读者读取序列计数,同步,看到有效性标志为真,读取数据,同步,并重新读取相同的序列计数,然后可能有一些希望它没有得到乱码数据。

如果你打算这样做,我会详尽地测试它。对我来说看起来似乎是合理的,但它可能不适用于从编译器到内存模型的所有特定实现。

另一个想法或检查此问题的方法是向缓冲区添加校验和并最后写入。

另请参阅搜索无锁算法,例如http://www.rossbencina.com/code/lockfree

顺便说一下,你可能想要一种方法让作者向睡觉的读者发出信号。您可能可以使用Posix信号量 - 例如让读者要求编写者在特定信号量到达给定序列号时调用sem_post(),或者当缓冲区变为有效时。

答案 4 :(得分:0)

另一种选择是坚持使用锁定,但要确保读者永远不要长时间持有锁定。读者可以通过在保持锁定但从写入缓冲区复制数据时不做任何其他操作来保持锁定时间短且可预测的时间。唯一的问题是低优先级读者可以在写入中途被更高优先级的任务中断,并且其解决方法是http://en.wikipedia.org/wiki/Priority_ceiling_protocol

鉴于此,如果编写器线程具有高优先级,则每个缓冲区要完成的最坏情况工作是编写器线程填充缓冲区,并且每个读取器线程将数据从该缓冲区复制到另一个缓冲区。如果你能够在每个周期中负担得起,那么写作者线程和一些读者数据复制将始终完成,而处理他们复制的数据的读者可能会或可能不会完成他们的工作。如果他们不这样做,他们就会落后,当他们下一次抓住锁并看看他们想要复制哪个缓冲区时会注意到这一点。

FWIW,我阅读实时代码的经验(当需要显示错误在那里,而不是在我们的代码中时)是,它是令人难以置信的,故意简单的,非常清晰的布局,并不一定如果您能够负担得起,那么为了获得直接锁定工作,一些显然毫无意义的数据复制可能是一个很好的协议。