有一个作家线程,定期从某处(实时地收集数据,但在问题中无关紧要)收集数据。 有很多读者从这些数据中读取。 通常的解决scheme是使用两个读写器锁和两个这样的缓冲区:
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
在这两种方法中,如果获取其他锁操作失败,则不进行交换,写入者会覆盖其以前的数据(因为写者是实时的,所以不能等待读者)所以在这种情况下,所有的读者都将丢失该帧数据的。
这并不是什么大问题,读者是我自己的代码,它们很短,所以使用双缓冲区,这个问题就解决了,如果出现问题,我可以使它成为三重缓冲区(或更多)。
问题是我想尽量减less延迟。 想象一下情况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
数据,如果只有作者可以在阅读器完成后进行交换,而不是等待下一个时期。 在这种情况下发生的事情是因为一个读者迟了一点,所有读者都错过了一个dataframe,而这个问题完全可以避免。
情况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
我试着把这些解决scheme混合在一起,所以作者在写完之后立即尝试交换缓冲区,如果不可能的话,在下一个阶段才会醒来。 所以像这样的东西:
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
这引入了抖动。 因为“less数时间”可能比“等待下一个时期”长,所以作者可能会错过时期的开始。
为了更清楚一点,这是我想要发生的事情:
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
我发现读取复制更新 ,据我所知,一直在为缓冲区分配内存,并释放它们,直到读者完成它们,这对我来说是不可能的,原因很多。 一,线程在内核和用户空间之间共享。 其次,使用RTAI,你不能在实时线程中分配内存(因为那么你的线程就会调用Linux的系统调用,从而打破实时性)(更不用说使用Linux自己的RCU实现是无用的出于同样的原因)
我也想过有一个额外的线程,在更高的频率尝试交换缓冲区,但这听起来不是这样一个好主意。 首先,它本身需要与作者同步;其次,我有许多这样的作家读者并行工作在不同的部分,每个作者的一个额外的线程似乎太多了。 对于所有作者来说,一个线程对于每个作者的同步来说似乎非常复杂
你用什么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; } }
这里发生的事情是,作者是否等待锁定或下一个时期,只要在下一个时期到来之前一定会醒来,这并不重要。 绝对超时保证这一点。
这不是三重缓冲应该解决的问题。 所以你有3个缓冲区,我们可以称它们为write1,write2和read。 写入线程在写入到写入1和写入2之间交替,确保它们永不阻塞,并且最后一个完整的帧始终可用。 然后,在读取线程中,在某个适当的位置(比如,在读取帧之前或之后),读取缓冲区将与可用的写入缓冲区一起翻转。
虽然这可以确保编写者永远不会阻塞(只需翻转两个指针即可,即使用CAS原子而不是锁),但是仍然存在读者不得不等待其他读者完成的问题与翻转之前的读取缓冲区。 我想这可以稍微解决RCU-esque和一个可用的翻转的缓冲池。
编辑以避免动态分配
我可能会使用循环队列…我会使用内置的__sync原子操作。 http://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html#Atomic-Builtins
如果你不想让作者等待,也许它不应该获得任何其他人可能拥有的锁定。 但是,我会让它执行某种同步,以确保它写入的内容真的被写出来 – 通常,大多数同步调用将导致执行内存刷新或屏障指令,但是细节取决于内存模型你的cpu和你的线程包的实现。
我会看看是否有任何其他同步原语更适合的东西,但如果推来推,我会让作家锁定和解锁一个别人从来没有使用过的锁。
然后,读者必须准备好不时地想念事情,并且必须能够检测到错过了什么东西。 我会将一个有效性标志和一个长序列号与每个缓冲区关联起来,让作者做一些类似于“清除有效性标志,增加序列数,同步,写入缓冲区,增加序列数,设置有效性标志,同步”的东西。 如果阅读器读取序列计数,则同步,看到有效性标志为真,读取数据,同步并重新读取相同的序列计数,也许有一些希望它没有得到乱码的数据。
如果你打算这样做,我会详尽地测试它。 看起来对我来说似乎是合理的,但它可能不适用于从编译器到内存模型的所有特定实现。
另一个想法,或者一个方法来检查这个,就是把一个校验和添加到你的缓冲区中,最后写下来。
另请参阅无锁定算法的搜索,例如http://www.rossbencina.com/code/lockfree
要做到这一点,你可能需要一种方式让作者向睡觉的读者发出信号。 您可能可以使用Posix信号量 – 例如让读写器在达到给定序列号或缓冲区有效时请求作者在特定信号量上调用sem_post()。
另一个选择是坚持锁定,但确保读者永远不会挂太久锁。 读者可以保持锁定时间短,并且可以通过在锁定该锁定的同时从写入器的缓冲区复制数据时不做别的事情来进行预测。 唯一的问题是,低优先级的读者可以在写入的中途被更高优先级的任务中断,并且治疗是http://en.wikipedia.org/wiki/Priority_ceiling_protocol 。
鉴于此,如果写入者线程具有高优先级,则每个缓冲区要做的最差的工作是写入者线程填充缓冲区,并且每个读取者线程将数据从该缓冲区复制到另一个缓冲区。 如果你在每个周期都能承受,那么写作者线程和一些阅读器数据拷贝将总是完成,而读者处理他们拷贝的数据可能会或不会完成他们的工作。 如果他们不这样做,他们会落后的,当他们下一把锁时,会注意到这一点,并看看他们想要复制哪个缓冲区。
FWIW,我阅读实时代码的经验(当需要显示错误是在那里,而不是在我们的代码中)时,它是令人难以置信和刻意的简单,非常清晰的布局,并不一定比它更有效需要满足截止日期,所以如果你能负担得起的话,一些明显无意义的数据复制,以便直接锁定工作可能是一个很好的交易。