C/C++ - POSIX兼容的共享内存环形缓冲区

3
我有一个应用程序,其中生产者和消费者(“客户端”)希望彼此发送广播消息,即n:m关系。所有这些都可以是不同的程序,因此它们是不同的进程而不是线程。
为了将n:m减少到更易维护的内容,我考虑设置像引入一个小型的中央服务器那样的设置。该服务器将提供每个客户端连接到的套接字。
每个客户端都会通过该套接字向服务器发送新消息-导致1:n
服务器还将提供一个由客户端只读共享内存。它将被组织为一个环形缓冲区,其中服务器将添加新消息并覆盖较旧的消息。
这将使客户端有一些时间来处理消息-但如果速度太慢,那就不幸了,反正它也不再相关了...
我认为这种方法的优点是我避免了同步以及不必要的数据复制和缓冲区层次结构,中央缓冲区应该足够,对吗?
到目前为止,这就是架构-我希望这有意义...
现在是实现更有趣的方面:
环形缓冲区中最新元素的索引是共享内存中的变量,客户端只需等待它更改即可。我想通过使用pthread_cond_wait()来释放CPU资源,而不是愚蠢的while( central_index == my_last_processed_index ) { /* do nothing */ }
但是这需要一个互斥锁,我认为我不需要-另一方面为什么pthread的条件变量函数需要互斥锁?给了我这样的印象,我最好问一下我的架构是否有意义并且能够这样实现...
您能否给我一个提示,所有这些都有意义并且可以工作吗?
(副注:客户端程序也可以用Perl和Python等常见脚本语言编写。因此,与服务器的通信必须在那里重新创建,因此不应该过于复杂或专有)

在这里要明确一点:消息是广播类型的,即“消费者”不会“消费”消息,它只是阅读它 - 所有“消费者”都必须阅读所有消息! - Chris
3个回答

3
如果我没记错的话,条件变量伴随互斥锁的原因是在POSIX下,发出条件变量信号会导致内核唤醒所有等待条件变量的线程。在这种情况下,消费者线程需要做的第一件事情是检查是否有可以消费的东西 - 通过访问生产者和消费者线程之间共享的变量。互斥锁保护此目的所使用的变量免受并发访问。当然,如果有很多消费者,其中n-1个人是不必要地被唤醒。
实施了上述精确安排后,选择要使用的IPC对象并不明显。我们在单独的进程中在高优先级实时线程之间缓冲音频,并且不想阻塞消费者。由于音频是实时产生和消耗的,我们已经定期在两端进行调度,如果没有要消费的(或要生产的空间),我们就会丢弃数据,因为我们已经错过了截止日期。
在您描述的安排中,您需要一个互斥锁来防止消费者同时消费排队的项目(相信我,在轻负载的SMP系统上,他们会这样做)。但是,您无需让生产者也参与竞争。
我不明白您关于消费者仅具有对共享内存的只读访问权限的评论。在经典的无锁环形缓冲区实现中,生产者写入队列尾指针,而消费者则写入头指针 - 所有参与方都需要能够读取两个指针。当然,您可以将队列头和尾放置在与队列数据本身不同的共享内存区域中。
此外,请注意,在实现此类环形缓冲区时,在SMP系统上存在理论数据一致性危险,即与头或尾指针相关的队列内容的回写可能会按照不同的顺序发生(它们在缓存中 - 通常是每个CPU内核)。还有其他与CPU之间的缓存同步有关的变体。为了防范这些问题,您需要使用内存、加载和存储屏障来强制排序。请参见维基百科上的内存屏障。您可以通过使用内核同步原语,如互斥锁和条件变量,来明确避免此风险。
C11原子操作可以帮助解决这个问题。

谢谢!但我希望所有消费者都能在广播类型的消息到达时被唤醒。也就是说,所有客户端都必须处理所有消息。因此,我只需要保护索引的所有字节(例如,对于典型的int32,为4个字节)一次性写入和读取。因此,我会在那里使用原子操作。 - Chris
那么如果是这种情况,谁会更新队列上的头指针(或者换句话说:生产者如何知道队列中剩余的容量?)。当消费者线程相互之间或与生产者之间运行时,您无法得到任何保证。 - marko
我能想到的唯一可行的方法是追踪消费每个消息的消费者数量,并在所有消费者都消费完毕后将“条目”释放回循环缓冲区。显然,对这种方法的反对意见是,在生产者生产消息时需要知道消费者的数量,并且动态添加或删除消费者会变得有趣。我认为解决此问题的强大(无锁)解决方案看起来像每个消费者的队列,其中插入了广播。 - marko
生产者并不关心消息是否被读取。缓冲区的大小为max_message_rate*max_interesting_time,这保证了在感兴趣的时间范围内所有的消息都是可用的。如果客户端消费得太慢,它将无法读取最旧的消息...由于内存有限,我需要处理缓冲区满的情况。以这种方式处理,我希望能够清晰明了。 - Chris

2
如果你的系统支持,使用sem_t可能会有一些不同的设计;一些POSIX系统仍然停留在2001年的版本上。
你可能并不需要强制使用互斥锁/条件对。这只是 POSIX 很久以前的设计。
现代 C、C11 和 C++、C++11 现在提供了原子操作,这是所有现代处理器都实现的特性,但大多数高级语言缺乏支持。原子操作是解决环形缓冲区竞争条件的部分答案。但它们还不足以解决问题,因为它们只能通过轮询进行主动等待,这可能不是你想要的。
作为 POSIX 的扩展,Linux 有futex,可以解决两个问题:通过使用原子操作避免更新竞争,并能够通过系统调用将等待者置于休眠状态。尽管 futex 被认为是日常编程中过于低级的工具,但我认为实际上使用它们并不太困难。我在这里写了一些东西。

“futex” 对于大多数人来说有点底层。例如,您不能在没有原子操作的情况下使用“futex”。 - Dietrich Epp
@DietrichEpp,这是一种普遍的观点,但我不认为它完全合理。特别是在现今这个时代,使用原子操作不应该是一个真正的问题(我们正在讨论的是带有gcc或类似编译器的Linux系统)。请查看我的编辑中的链接。 - Jens Gustedt

2

据我所知,在pthread_cond_wait()中确实需要使用互斥锁。原因是pthread_cond_wait()不是原子操作。在调用期间,条件变量可能会发生变化,除非它受到互斥锁的保护。

如果你可以忽略这种情况,客户端可能会错过消息1,但当后续消息发送时,客户端将醒来并找到两条要处理的消息。如果无法接受,则使用互斥锁。


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