实时查看循环缓冲区中的数据

时间:2009-10-22 12:42:00

标签: c++ c model-view-controller stl

我有一个传入的消息流,并且想要一个允许用户滚动消息的窗口。

这是我目前的想法:

  • 传入的消息进入单个生产者单个消费者队列
  • 一个线程将它们读出并将它们放入一个带有顺序id的循环缓冲区
  • 这样我可以将多个传入流安全地放置在循环缓冲区中,并将输入解耦
  • 互斥用于协调UI和线程之间的循环缓冲区访问
  • 从线程到UI的两个通知,一个用于第一个id,另一个用于缓冲区中的最后一个ID。
  • 这允许UI确定它可以显示什么,它需要访问的循环缓冲区的哪些部分,删除被覆盖的消息。它只访问以当前大小和滚动位置填充窗口所需的消息。

我对用户界面中的通知感到不满意。它将以高频率生成。这些可能会排队或以其他方式受到限制;延迟不应该影响第一个id,但处理最后一个id的延迟可能会导致角落情况出现问题,例如查看完整缓冲区的最后一部分,除非 UI复制它显示的消息,我想避免。

这听起来像是正确的做法吗?任何调整都可以让它变得更加美味?

2 个答案:

答案 0 :(得分:3)

(请参阅下面的Effo EDIT,不推荐使用此部分)如果线程与每个UI之间存在队列,则不需要使用环形缓冲区。

当消息到达时,线程会弹出它并相应地将其推送到UI的队列。

此外,每个UI.Q也可以原子操作。不需要互斥锁。另一个好处是每条消息只被复制了两次:一个是低级别队列,另一个是显示,因为将消息存储到其他地方是不必要的(只需将指针从低级别队列分配给UI.Q应该足够了如果是C / C ++)。

到目前为止唯一的问题是,当消息流量很大时,UI.Q的长度可能不够运行。根据这个问题,您可以使用动态长度队列,也可以让UI本身将溢出的消息存储到posix内存映射文件中。使用posix映射时效率很高,即使您正在使用文件并需要进行额外的消息复制。但无论如何它只是异常处理。队列可以设置为合适的大小,这样通常你会获得出色的表现。关键是当UI需要将溢出的消息存储到映射文件时,它应该执行高度并发的操作,以便它不会影响低级别队列。

我更喜欢动态调整队列提案。看来我们在现代PC上有很多记忆。

请参阅http://code.google.com/p/effonetmsg/downloads/list处的文档EffoNetMsg.pdf,以了解有关无锁,队列设施和高度并发编程模型的更多信息。


Effo EDIT @ 2009oct23:显示一个支持随机消息访问的分阶段模型,用于滚动消息查看器。

                         +---------------+ 
                     +---> Ring Buffer-1 <---+
                     |   +---------------+   |
                  +--+                       +-----+
                  |  |   +---------------+   |     |
                  |  +---> Ring Buffer-2 <---+     |
                  |      +---------------+         |
                  |                                |
          +-------+-------+            +-----------+----------+
          |   Push Msg &  |            |   GetHeadTail()      |
          |  Send AckReq  |            |  & Send UpdateReq    |
          +---------------+            +----------------------+
          |App.MsgStage() |            |   App.DisPlayStage() |
          +-------+-------+            +-----------+----------+
                  | Pop()                          | Pop()         
 ^              +-V-+                            +-V-+ 
 | Events       | Q |    Msg Stage |             | Q |  Display Stage
 | Go Up        | 0 |   Logic-Half |             | 1 |   Logic-Half      
-+------------- |   | -------------+------------ |   | ---------------
 | Requests     |   |    I/O-Half  |             |   |    I/O-Half
 | Move Down    +-^-+              |             +-^-+   
 V                | Push()                         |     
   +--------------+-------------+                  |
   |   Push OnRecv Event,       |          +-------+-------+
   | 1 Event per message        |          |               | Push()
   |                            |   +------+------+ +------+------+
   |  Epoll I/O thread for      |   |Push OnTimer | |Push OnTimer |
   |multi-messaging connections |   |  Event/UI-1 | |  Event/UI-2 |
   +------^-------^--------^----+   +------+------+ +------+------+
          |       |        |               |               |                   
Incoming msg1    msg2     msg3        Msg Viewer-1    Msg Viewer-2              

要点:

1您了解不同的高度并发模型,具体如上图所示,分阶段模型;这样你就会知道为什么它会快速运转。

2两种I / O,一种是Messaging或Epoll Thread,如果是C / C ++和GNU Linux 2.6x;另一种是显示,如绘图屏幕或打印文本等。因此,2种I / O被处理为2阶段。请注意,如果是Win / MSVC,请使用完成端口而不是Epoll。

3仍然有2个消息复制,如前所述。 a)Push-OnRecv生成消息(如果是C / C ++,则为“CMsg * pMsg = CreateMsg(msg)”); b)UI相应地从它的环形缓冲区读取和复制消息,并且只需要复制更新的消息部分,而不是整个缓冲区。注意队列和环形缓冲区只存储一个消息句柄(“queue.push(pMsg)”或“RingBuff.push(pMsg)”,如果是C / C ++),任何老化的消息都将被删除(“pMsg-&gt; Destroy()“如果是C / C ++)。通常,MsgStage()会在将其推入环形缓冲区之前重建Msg Header。

4在OnTimer事件之后,UI将从上层接收更新,其中包含环buff的新头/尾指示符。因此UI可以相应地更新显示。 Hope UI有一个本地的msg缓冲区,因此不需要复制整个环形缓冲区,只需更新即可。见上文第3点。如果需要在环形缓冲区上执行随机访问,您可以让UI生成OnScroll事件。实际上,如果UI具有本地缓冲区,则可能不需要OnScroll。无论如何,你可以做到。注意UI将确定是否丢弃老化的消息,比如生成OnAgedOut事件,以便可以正确安全地操作环形缓冲区。

5确切地说,OnTimer或OnRecv是事件名称,OnTimer(){}或OnRecv(){}将在DisplayStage()或MsgStage()中执行。同样,事件向上发展,请求向下游发送,这可能与您之前或之前看到的不同。

6个Q0和2个环形缓冲器可以实现为无锁设施,以提高性能,因为单个生产者和单个消费者;无需锁定/互斥锁。而Q1是不同的东西。但我相信你可以通过稍微改变上面的设计数字,使它成为单一生产者和单一消费者,例如添加Q2以便每个UI都有一个队列,DisplayStage()可以只轮询Q1和Q2来正确处理所有事件。注意Q0和Q1是事件队列,请求队列未在上图中显示。

7 MsgStage()和DisplayStage()按顺序位于单个StagedModel.Stage()中,比如主线程。 Epoll I / O或Messaging是另一个线程,MsgIO线程,每个UI都有一个I / O线程,比如显示线程。所以在上图中,共有4个线程同时运行。 Effo测试过只有一个MsgIO线程应该足以满足多个liseners和数千个消息传递客户端。

再次,请参阅http://code.google.com/p/effonetmsg/downloads/list处的文档EffoNetMsg.pdf或http://code.google.com/p/effoaddon/downloads/list处的EffoAddons.pdf,以了解有关高度并发编程模型和网络消息传递的更多信息;请参阅http://code.google.com/p/effocore/downloads/list处的EffoDesign_LockFree.pdf,了解有关无锁设施(如无锁队列和无锁环缓冲区)的更多信息。

答案 1 :(得分:1)

GUI的通知不应包含ID,即当前值。相反,它应该只是说“当前值已经改变”,然后让GUI读取值:因为在发送通知和读取值的GUI之间可能存在延迟,并且您希望GUI读取当前值(而不是潜在的陈旧价值)。您希望它是异步通知。

你也可以节制通知,例如:每秒发送不超过5或20(如果需要,可以将通知延迟最多50到200毫秒)。

此外,GUI将不可避免地制作所显示消息的副本,因为屏幕上会显示消息的副本(在显示驱动程序中)!至于GUI是否将副本复制到自己的私有RAM缓冲区中,虽然您可能不想复制整个消息,但您可能会发现设置更安全/更容易,您可以根据需要复制尽可能多的消息绘制/重新绘制显示(因为你不能一次在屏幕上绘画,这意味着你需要复制的数据量是微不足道的。)