【问题标题】:Viewing data in a circular buffer in real-time实时查看循环缓冲区中的数据
【发布时间】:2009-10-23 16:36:53
【问题描述】:

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

这是我目前的想法:

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

我对 UI 中的通知不满意。它将以高频率生成。这些可以排队或以其他方式限制;延迟不应影响第一个 id,但处理最后一个 id 的延迟可能会在极端情况下导致问题,例如查看完整缓冲区的最末端除非 UI 复制了它显示的消息,这我想避免。

这听起来像是正确的方法吗?有什么可以让它更美味的调整吗?

【问题讨论】:

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


    【解决方案1】:

    (请参阅下面的 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,使用 Completion Port 而不是 Epoll。

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

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

    5 没错,OnTimer 或 OnRecv 是 Event 名称,而 OnTimer(){} 或 OnRecv(){} 将在 DisplayStage() 或 MsgStage() 中执行。同样,事件向上,请求向下,这可能与您之前看到或看到的不同。

    6 个 Q0 和 2 个环形缓冲区可以实现为无锁设施以提高性能,因为单一生产者和单一消费者;不需要锁/互斥锁。而 Q1 则有所不同。但我相信你也可以通过稍微改变上面的设计图来使其成为单一生产者和单一消费者,例如添加 Q2 以便每个 UI 都有一个队列,并且 DisplayStage() 可以轮询 Q1 和 Q2 以正确处理所有事件。注意 Q0 和 Q1 是 Event-Queue,Request-Queue 没有在上图中显示。

    7 MsgStage() 和 DisplayStage() 依次位于单个 StagedModel.Stage() 中,例如主线程。 Epoll I/O 或 Messaging 是另一个线程,MsgIO 线程,每个 UI 都有一个 I/O 线程,比如 Display Thread。所以在上图中,总共有 4 个线程同时运行。 Effo 测试了一个 MsgIO 线程应该足以满足多监听器和数千个消息传递客户端的需求。

    再次,请参阅 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 以了解有关无锁设施的更多信息,例如无锁队列和无锁环形缓冲区。

    【讨论】:

    • 是的,如果我将 UI.Q 与显示代码放在一起,事情可能会更容易。这可能仍然是最好的循环缓冲区,因为旧的东西会在溢出时被删除。 UI 需要随机访问 UI.Q 的任何部分以向上/向下滚动窗口。我来看看你的无锁队列信息。
    【解决方案2】:

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

    您还可以限制通知,例如每秒发送不超过 5 或 20 个(如有必要,最多可延迟 50 到 200 毫秒的通知)。

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

    【讨论】:

    • 是的,实际上 UI 需要重绘的消息数量非常少,大约 1-50 取决于显示模式,并且它们的大小可能每个都只有几百字节平均,所以副本不会是世界末日。
    猜你喜欢
    • 2012-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多