【问题标题】:Schedule sending messages to consumers at different rate安排以不同的速率向消费者发送消息
【发布时间】:2018-11-14 20:45:24
【问题描述】:

我正在寻找消息调度的最佳算法。我所说的消息调度是指当​​我们有许多消费者以不同的速率在总线上发送消息的一种方式。

示例: 假设我们有数据 D1 到 Dn . D1 每 5ms 发送给多个消费者 C1 每 19ms C2 每 30ms C3 每 30ms Cn 每 Rn ms . Dn 每 10ms 发送到 C1,C2 每 31ms,Cn 每 50ms

以最佳性能(CPU、内存、IO)调度此操作的最佳算法是什么?

问候

【问题讨论】:

    标签: algorithm optimization scheduling


    【解决方案1】:

    我能想到很多选择,每一种都有自己的成本和收益。真正归结为您的需求是什么——真正为您定义“最佳”的东西。我在下面对几种可能性进行了伪编码,希望能帮助您入门。

    选项 1:每个时间单位(在您的示例中为毫秒)执行以下操作

    func callEachMs
        time = getCurrentTime()
        for each datum
            for each customer
                if time % datum.customer.rate == 0
                    sendMsg()
    

    这具有不需要一致存储的内存的优点 - 您只需在每个时间单位检查您是否应该发送消息。这也可以处理未在time == 0 发送的消息——只需存储消息最初发送的时间以速率为模,并将条件替换为if time % datum.customer.rate == data.customer.firstMsgTimeMod。

    此方法的一个缺点是它完全依赖于始终以 1 毫秒的速率被调用。如果 CPU 上的另一个进程导致延迟并且它错过了一个周期,那么您可能会完全错过发送消息(而不是延迟发送)。

    选项 2:维护一个元组列表,其中每个条目代表需要在该毫秒内完成的任务。使您的列表至少与最长速率除以时间单位一样长(如果您的最长速率是 50 毫秒并且您要以毫秒为单位,那么您的列表必须至少为 50 长)。当你启动你的程序时,放置第一个消息将被发送到队列中。然后每次发送消息时,在下次发送时更新该列表。

    func buildList(&list)
        for each datum
            for each customer
                if list.size < datum.customer.rate
                    list.resize(datum.customer.rate+1)
                list[customer.rate].push_back(tuple(datum.name, customer.name))
    
    func callEachMs(&list)
        for each (datum.name, customer.name) in list[0]
            sendMsg()
            list[customer.rate].push_back((datum.name, customer.name))
        list.pop_front()
        list.push_back(empty list)
    

    这具有避免许多不必要的模数计算选项 1 所需的优势。但是,这会带来内存使用量增加的成本。如果您的各种消息的速率存在很大差异,则此实现也不会有效(尽管您可以修改它以更有效地处理具有更长速率的算法)。而且它仍然必须每毫秒调用一次。

    最后,您必须非常仔细地考虑您使用的数据结构,因为这将对其效率产生巨大影响。因为您在每次迭代时从前面弹出并从后面推送,并且列表是固定大小的,所以您可能希望实现 circular buffer 以避免不必要的值移动。对于元组列表,由于它们只被迭代(不需要随机访问),并且经常添加,因此单链表可能是您的最佳解决方案。

    .

    显然,您可以通过更多方式来做到这一点,但希望这些想法可以帮助您入门。另外,请记住,您运行它的系统的性质可能会对哪种方法效果更好,或者您是否想要完全做其他事情有很大的影响。例如,这两种方法都要求能够以一定的速率可靠地调用它们。我也没有描述并行化实现,如果您的应用程序支持它们,这可能是最好的选择。

    【讨论】:

    • 我认为第一个命题是如此有限,因为它是指数级的复杂性。因此,当数据/消费者数量增加时,性能会大大降低。从我的角度来看,我已经编写了第二个代码(在发送之前准备时间表),但我在下一个答案中分享了一些优化。问题还在于,当消费者数量以非常不同的速度增加时,调度表也会增加。
    • 第一个命题如何以指数复杂度增长?我很确定对于 m 个数据和 n 个客户来说,这是 O(mn) 最坏的情况。正如我所说,第一个版本的主要目的是使用尽可能少的内存,这可能对嵌入式系统有用。您不太清楚应用程序是什么(“最佳性能(CPU、内存、IO)”非常模糊),所以我尝试为不同的优先级提供多个选项。
    • 是的,这是一个错误,我会修复它,复杂度为 O(nm)(线性复杂度)。
    【解决方案2】:

    就像 Helium_1s2 描述的那样,还有第二种方法,它基于我所谓的调度表,这就是我现在使用的方法,但这种解决方案有其局限性。

    假设我们有一个数据要发送,两个消费者 C1 和 C2 :

    如您所见,我们必须提取调度表,并且必须确定重复传输周期和 IDLE MINIMUM PERIOD 的值。事实上,在 1ms 或 1ns 或 1mn 或 1h(视情况而定)的最小时间周期上循环是没有用的,但它并不总是最好的时间段,我们可以如下优化此循环。

    例如一个(C1 在 6 和 C2 在 9),我们注意到有一个循环从 0 到 18 重复。两个连续发送事件的最小差异等于 3。 所以:

    HCF(6,9) = 3 = IDLE MINIMUM PERIOD
    LCM(6,9) = 18 = transmission cycle length
    LCM/HCF = 6 = size of our schedule table
    

    时间表是:

    发送循环看起来像:

    while(1) {
      sleep(IDLE_MINIMUM_PERIOD); // free CPU for idle min period
      i++; // initialized at 0
      send(ScheduleTable[i]);
      if (i == sizeof(ScheduleTable)) i=0;
    }
    

    这种方法的问题是,如果 LCM 增长,这个数组会增长,如果我们有不好的组合,比如 rate = 素数等。

    【讨论】:

    • 此解决方案的内存使用将变得非常有问题,尤其是如果您的任何费率都是素数。您应该考虑实现一个循环缓冲区,因为在这种情况下内存使用只是最长速率的长度除以最大公因数。并且循环缓冲区意味着你仍然有 O(1) 的随机访问,所以你不会有时间效率问题。
    • 例如,假设您以 5、7、15 和 25 的费率发布内容。即 LCM 为 525,GCF 为 1。因此,您的队列长度为 525。而循环缓冲区只有 25 长。
    • 是的,采用最大速率大小的数组(循环缓冲区或其他)并计算下一个事件槽的下一个索引在某些情况下是一个很好的解决方案,但在其他一些情况下,循环使用的内存缓冲解决方案将占用大量内存,例如(关于地理定位的真实示例)200 毫秒、1000 毫秒和 1 小时(3600 公里)的循环缓冲区,您的大小必须为 3600.000 长,但其他大小为 18000。当数据数量增加时,每个解决方案都会达到极限。我认为还有另一种方法,因为这种问题存在于许多领域,并且肯定有其他解决方案
    • 从我上面的评论中,“你应该考虑实现一个循环缓冲区,因为在这种情况下内存使用只是最长速率的长度除以最大公因数。”事实上,循环缓冲区使用的内存将始终小于或等于 LCM/GCF 解决方案使用的内存量。
    • 在您给出的示例中,GCF 为 200,LCM 为 3600000,最大值为 3600000。因此循环缓冲区将使用 max/GCF=3600000/200=18000 内存,而您的方法将使用 LCM/GCF=3600000/200=18000 内存。在这种情况下,LCM 等于最大值,因此这些方法使用相同的内存量。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-01
    • 2020-12-09
    • 1970-01-01
    • 2019-05-20
    • 1970-01-01
    • 2014-10-23
    相关资源
    最近更新 更多