繁体   English   中英

什么是多线程环境中的忙自旋?

[英]What is busy spin in a multi-threaded environment?

什么是多线程环境中的“Busy Spin”?

它如何有用以及如何在多线程环境中用 java 实现?

它在哪些方面有助于提高应用程序的性能?

其他一些答案忽略了忙等待的真正问题。

除非您谈论的应用程序涉及节省电力,否则消耗 CPU 时间本身并不是一件坏事。 只有当有其他一些线程或进程准备好运行时,这才是糟糕的。 当准备运行的线程之一是忙等待循环正在等待的线程时,这真的很糟糕。

这才是真正的问题。 在普通操作系统上运行的普通用户模式程序无法控制哪些线程在哪些处理器上运行,普通操作系统无法区分忙于等待的线程和正在工作的线程之间的区别,并且即使操作系统知道线程正在忙等待,它也无法知道线程在等待什么。

因此,忙碌的服务员完全有可能等待许多毫秒(实际上是永恒),等待一个事件,而唯一可以使事件发生的线程坐在边线(即,在运行队列中)等待轮到使用CPU了。

忙等待通常用于严格控制哪些线程在哪些处理器上运行的系统中。 当您知道导致事件发生的线程实际上在不同的处理器上运行时,忙等待可能是等待事件的最有效方式。 当您为操作系统本身编写代码时,或者当您编写在实时操作系统下运行的嵌入式实时应用程序时,通常就是这种情况。


凯文·沃尔特斯 (Kevin Walters) 写过等待时间很短的案例。 在普通操作系统上运行的受 CPU 限制的普通程序可能被允许在每个时间片中执行数百万条指令。 因此,如果程序使用自旋锁来保护仅由几条指令组成的临界区,那么任何线程在处于临界区时都不太可能丢失其时间片。 这意味着,如果线程A发现自旋锁锁定,则很可能该线程B,保持所述锁,实际上一个不同的CPU上运行。 这就是为什么当您知道它将在多处理器主机上运行时,在普通程序中使用自旋锁是可以的。

忙等待或自旋是一种技术,其中进程重复检查条件是否为真,而不是调用等待或睡眠方法并释放 CPU。

1.它主要用于多核处理器,条件很快就会成立,即毫秒或微秒

2.不释放CPU的好处是,所有缓存的数据和指令不受影响,可能会丢失,如果这个线程在一个内核上挂起并带回另一个线程

Busy spin 是一种在不释放 CPU 的情况下等待事件的技术。 通常这样做是为了避免丢失 CPU 缓存中的数据,如果线程在其他内核中暂停和恢复,则数据会丢失。

因此,如果您在一个低延迟系统上工作,您的订单处理线程当前没有任何订单,而不是休眠或调用wait() ,您可以循环然后再次检查队列中是否有新消息。 只有当您需要等待非常短的时间(例如微秒或纳秒)时,它才有用。

LMAX Disrupter 框架是一个高性能的线程间消息传递库,它有一个 BusySpinWaitStrategy,它基于这个概念,并为在屏障上等待的 EventProcessor 使用忙碌的自旋循环。

“忙旋转”在一个线程中不断循环,以查看另一个线程是否已完成某些工作。 这是一个“坏主意”,因为它只是在等待而消耗资源。 最繁忙的旋转甚至没有睡眠,而是尽可能快地旋转等待工作完成。 直接通过工作完成通知等待线程并让它休眠直到那时浪费更少。

请注意,我称之为“坏主意”,但它在某些情况下用于低级代码以最小化延迟,但这在 Java 代码中很少(如果有的话)需要。

从性能的角度来看,忙于旋转/等待通常是一个坏主意。 在大多数情况下,最好在准备好跑步时睡觉并等待信号,而不是进行旋转。 假设有两个线程,线程 1 正在等待线程 2 设置变量(例如,它一直等到var == true 。然后,它会忙

while (var == false)
    ;

在这种情况下,您将占用线程 2 可能正在运行的大量时间,因为当您醒来时,您只是在无意识地执行循环。 因此,在您等待此类事情发生的情况下,最好让线程 2 拥有所有控制权,让您自己进入睡眠状态,并在完成后让它唤醒您。

但是,在极少数情况下,您需要等待的时间很短,实际上使用 spinlock更快 这是因为执行信令功能需要时间; 如果使用的旋转时间少于执行信令所需的时间,则旋转是可取的。 因此,以这种方式它可能是有益的,并且实际上可以提高性能,但这绝对不是最常见的情况。

Spin Waiting 是您不断等待条件成立。 相反的是等待信号(如notify() 和wait() 引起的线程中断)。

有两种等待方式,第一种是半主动(sleep/yield)和主动(忙等待)。

在忙等待时,程序会使用特殊操作码(如 HLT 或 NOP 或其他耗时操作)主动空闲。 其他用途只是一个 while 循环检查条件是否成立。

JavaFramework 提供了 Thread.sleep、Thread.yield 和 LockSupport.parkXXX() 方法让一个线程交出 CPU。 Sleep 等待特定的时间量,但即使指定了纳秒,也总是需要超过一毫秒。 LockSupport.parkNanos(1) 也是如此。 Thread.yield 允许我的示例系统(win7 + i5 mobile)的分辨率为 100ns。

yield 的问题在于它的工作方式。 如果系统被完全利用,在我的测试场景中可能需要 800 毫秒(100 个工作线程都不确定地计数一个数字 (a+=a;))。 由于yield 释放cpu 并将线程添加到其优先级组内所有线程的末尾,因此yield 因此不稳定,除非cpu 未被使用到一定程度。

忙等待将阻塞 CPU(核心)数毫秒。

Java 框架(检查 Condition 类实现)使用小于 1000ns(1 微秒)的活动(忙)等待时间。 在我的系统中,System.nanoTime 的平均调用需要 160ns,所以忙于等待就像检查条件在 nanoTime 上花费 160ns 并重复。

所以基本上 Java 的并发框架(队列等)有类似在微秒旋转下等待并在 N 粒度内达到等待周期,其中 N 是检查时间约束的纳秒数并等待一毫秒或更长时间(对于我目前的系统)。

因此,主动忙等待增加了利用率,但有助于系统的整体反应性。

在消耗 CPU 时间的同时,应使用特殊指令来降低执行耗时操作的内核的功耗。

忙自旋只不过是循环直到线程完成。 例如,您说有 10 个线程,并且您想等待所有线程完成然后想继续,

while(ALL_THREADS_ARE_NOT_COMPLETE);
//Continue with rest of the logic

例如在 Java 中,您可以使用ExecutorService管理多个线程

    ExecutorService executor = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 10; i++) {
        Runnable worker = new WorkerThread('' + i);
        executor.execute(worker);
    }
    executor.shutdown();
    //With this loop, you are looping over till threads doesn't finish.
    while (!executor.isTerminated());

这是一个忙碌的旋转,因为它消耗资源,因为 CPU 并不理想,而是继续在循环中运行。 我们应该有机制来通知主线程(父线程)来指示所有线程都完成了,它可以继续执行剩余的任务。

在前面的示例中,您可以使用不同的机制来提高性能,而不是繁忙的旋转。

   ExecutorService executor = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 10; i++) {
        Runnable worker = new WorkerThread('' + i);
        executor.execute(worker);
    }
    executor.shutdown();
    try {
           executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
    } catch (InterruptedException e) {
        log.fatal("Exception ",e);
    }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM