【问题标题】:busy spin to reduce context switch latency (java)忙旋转以减少上下文切换延迟(java)
【发布时间】:2012-12-18 21:33:08
【问题描述】:

在我的应用程序中,有几个服务在它们自己的线程上处理信息,当它们完成后,它们会将消息发布到下一个服务,然后继续在自己的线程上完成其工作。消息的移交是通过 LinkedBlockingQueue 完成的。切换通常需要 50-80 us(从将消息放入队列直到消费者开始处理消息)。 为了加快最重要服务的移交,我想使用繁忙的自旋而不是阻塞方法(我有 12 个处理器内核并希望将 3 个专用于这些重要服务)。 所以.. 我将 LinkedBlockingQueue 更改为 ConcurrentLinkedQueue

做了

for(;;)
{
 Message m = queue.poll();
 if( m != null )
  ....
}

现在.. 结果是第一个消息传递需要 1 us,但随后延迟在接下来的 25 次切换中增加,直到达到 500 us,然后延迟突然回到 1 us,并且开始增加.. 所以我有 25 次迭代的延迟周期,其中延迟从 1 us 开始,到 500 us 结束。 (消息每秒传递大约 100 次)

平均延迟为 250,这并不是我想要的性能提升。

我还尝试使用 LMAX Disruptor 环形缓冲区而不是 ConcurrentLinkedQueue。该框架在繁忙的自旋实现中具有自己的构建和完全不同的队列实现,但结果是相同的。所以我很确定这不是队列的错,也不是我滥用某些东西..

问题是……这到底是怎么回事?为什么我会看到这种奇怪的延迟周期?

干杯!!

【问题讨论】:

  • 您是否跟踪产生空白的旋转周期数?也许延迟与线程调度程序有关。你如何确保线程拥有核心的 100% 所有权?
  • 这听起来与调度程序有关。你在什么操作系统上运行它?
  • '消息每秒传递大约 100 次' - 那是 10 毫秒,对吗?因此,使用阻塞队列,50-80us 添加到 10ms。减少增加的延迟有那么重要吗?即使是这样,在队列弹出时旋转(与用于在推送/弹出对象时短暂保护队列的自旋锁不同)也是 CPU/内存带宽黑洞 - 没有希望。
  • #Marko,钓鱼者。是的,我也怀疑操作系统调度程序,我正在运行 windows 和 java,因此无法将核心分配给进程。我唯一能做的就是不要运行比我拥有的内核更多的线程。
  • #Martin,消息以 10 毫秒的间隔发布,但是对于一个要创建的事件进行处理并到达其目的地大约需要 10 毫秒。 500我们。其中涉及上述类型的 3 个上下文切换。所以多达一半的延迟仅由切换线程组成。是的,也许它是无望的.. :)

标签: java multithreading performance queue


【解决方案1】:

据我所知,如果线程调度程序检测到该线程正在大量使用 CPU,它可以故意将线程暂停更长的时间——以便更公平地在不同线程之间分配 CPU 时间。尝试在队列为空后在消费者中添加LockSupport.park(),在添加消息后在生产者中添加LockSupport.unpark() - 这可能会使延迟变小;与阻塞队列相比,它是否真的更好是一个大问题。

【讨论】:

  • 抖动模式消失了,但它实际上增加了延迟(与blockingQueue相比增加了一倍)。
  • 调度程序无法确定正在运行的线程实际上没有做任何有用的工作,因此,例如,很乐意运行 [cores] 轮询线程,因此拒绝 CPU 给生产者/ s,否则可能会发布更有用的工作。除了吸收 CPU 内核和加热房间外,轮询队列还用大量的 gunge 原子操作加载处理器间内存路径。可怕..
  • @Martin - 显然调度程序无法确定线程在做什么,但它可以跟踪它实际做某事的时间(处于运行状态);我的观点是,在运行状态中花费的时间比其他线程多得多的线程可能会在某个时候获得更短的时间片,或者被故意延迟。我确实同意旋转通常是一个坏主意。
  • 好的,所以如果非阻塞队列不能提高性能,请尝试从不同的角度解决该问题。在每个线程中做更多的事情怎么样 - 1 个较大的线程池一次执行 3 个步骤,而不是 3 个较小的线程池每个执行 1 个步骤?您提到了大约 500 微秒的总体延迟,这表明您主要进行计算和日志记录(没有远程调用、SQL 等)——在这种情况下,广泛的多线程不是您的朋友。
  • 实际上还有很多其他因素促使线程模型保持现状。改变架构是可能的,但这篇文章的主题应该集中在如何减少上下文切换延迟,不仅作为解决手头问题的解决方案,而且作为一般问题的解决方案。
【解决方案2】:

如果您确实需要按照您描述的方式完成工作(而不是 Andrey Nudko 在 1 月 5 日 13:22 回复的方式),那么您肯定需要从其他角度看待问题。

只是一些提示:

  1. 尝试检查您的整体环境(在 JVM 之外)。例如:

  2. JVM 内部的“问题”

  3. 尝试更改线程优先级:Setting priority to Java's threads

【讨论】:

    【解决方案3】:

    这只是疯狂的猜测(因为正如其他人所提到的,您没有收集有关队列长度、失败的轮询、空轮询等的任何信息):

    我用力阅读了source of ConcurrentLinkedQueue,或者更确切地说,只是简单地翻阅了一两分钟。轮询并不是您微不足道的 O(1) 操作。可能是您正在遍历多个已过时的节点,保持为空;并且可能存在额外的临时状态,这些状态涉及链接到自身的节点作为下一个节点,作为队列陈旧/删除的指示。可能是由于线程调度,队列开始堆积垃圾。尝试按照代码中提到的抽象算法的链接:

    Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue,作者 Maged M. Michael 和 Michael L. Scott(链接包含 PDF 和伪代码)。

    【讨论】:

      【解决方案4】:

      这是我的 2 美分。如果您在基于 linux/unix 的系统上运行,则有一种方法可以将某个 cpu 专用于某个线程。从本质上讲,您可以让操作系统忽略该 cpu 进行任何调度。检查 cpu 的隔离级别

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-10-31
        • 2013-07-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-04-10
        • 2012-08-10
        • 2021-10-22
        相关资源
        最近更新 更多