在多线程环境中,什么是忙等待?

24

在多线程环境中,“忙等待”是什么?

它如何有用,如何在Java多线程环境中实现?

它以何种方式有助于提高应用程序的性能?


3
一般而言,这是纯粹的邪恶。 - Hot Licks
(也许您可以提供一个参考,说明它为什么“有用”。) - Hot Licks
7个回答

21

其他回答中忽略了“忙等待”的真正问题。

除非你在处理需要节约电力的应用程序,否则烧掉CPU时间本身并不是什么坏事。只有当存在其他准备好运行的线程或进程时,它才是“坏事”。而当一个处于准备就绪状态的线程卡在正在进行忙等待的循环中时,情况变得更加严重。

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

因此,在一个忙等待的循环中,等待事件的线程可能会等待多毫秒(几乎是永远),而唯一能够发生该事件的线程却在一旁等待使用CPU。

忙等待通常用于系统中需要精细控制线程在哪个处理器上运行的情况下。当你知道将引起事件的线程实际上在另一个处理器上运行时,忙等待可能是等待事件最高效的方式。这通常发生在编写操作系统本身的代码或编写在实时操作系统下运行的嵌入式实时应用程序时。


Kevin Walters提到了一个等待时间非常短的情况。在一个普通操作系统上运行的CPU密集型普通程序每个时间片可能会被允许执行数百万条指令。因此,如果程序使用自旋锁来保护由几条指令组成的临界区域,那么当线程处于临界区域时,它很有可能不会失去时间片。也就是说,如果线程A发现自旋锁已经被锁定,那么持有锁的线程B很有可能实际上正在不同的CPU上运行。这就是为什么在知道程序将在多处理器主机上运行时,在普通程序中使用自旋锁是可以接受的原因。


13

Busy spin是一种不释放CPU等待事件的技术之一。它通常用于避免在线程暂停并在其他核心中恢复时丢失CPU缓存中的数据。

因此,如果您正在处理低延迟系统,其中您的订单处理线程当前没有任何订单,而不是睡眠或调用wait(),您可以循环并再次检查新消息队列。仅当您需要等待非常短的时间(例如微秒或纳秒)时才有益。

LMAX Disrupter framework是一个高性能的线程间通信库,具有基于这个概念和使用忙旋转循环的BusySpinWaitStrategy,用于在障碍上等待EventProcessors。


12

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

1.它主要在多核处理器中有用,其中条件很快就会变为真,即在毫秒或微秒内。

2.不释放CPU的优点是,所有缓存的数据和指令都保持不受影响,如果该线程被挂起在一个核上并带回到另一个线程,则可能会丢失这些数据和指令。


6
“Busy spin”指的是一个线程不停地循环,以便查看另一个线程是否完成了一些工作。这是一个“坏主意”,因为它会占用资源,而且只是在等待。最忙碌的旋转甚至没有睡眠,而是尽可能快地旋转,等待工作完成。更节省资源的做法是直接通过完成工作来通知等待的线程,并让它在此之前休眠。
注意,我称这是一个“坏主意”,但在某些情况下,它在低级代码中被用于最小化延迟,但这在Java代码中很少需要(如果需要的话)。

谢谢回复,你的意思是说,在线程等待的情况下,使用信号量(wait和notify/notifyAll)比忙碌自旋更好?实际上,我不明白为什么在线程等待的情况下会出现忙碌自旋。 - Bravo
2
假设您有一堆生产者线程将工作项推入(同步的)队列,而您有一个消费者线程从队列中弹出项目以对其进行服务。当队列为空时,您的消费者线程不应该轮询工作项队列。相反,当添加新项时以某种方式向消费者发出信号。 - Andreas
@Andreas,轮询(我认为这里应该是繁忙自旋)会消耗不必要的CPU资源,因为它只是不断地检查保存项目的队列。实际上,我在一些disruptor中看到了这个问题,请纠正我,如果有不同的地方。 - Bravo
@Bravo,你提到的Disruptor实际上是这个答案的一个很好的例子:它既有busy spin实现(高CPU使用率),也有另一种wait for notification实现(低CPU使用率)。 - vanOekel

3

从性能角度来看,繁忙的自旋/等待通常是不好的。在大多数情况下,最好睡眠并等待信号,而不是进行自旋。考虑这样一种情况,有两个线程,线程1正在等待线程2设置一个变量(比如说,它会等待直到 var == true)。然后,它将通过简单地执行

while (var == false)
    ;

在这种情况下,你将花费大量时间执行循环,而此时第二个线程可能正在运行。因此,在等待某些事情发生的情况下,最好让第二个线程完全控制并让自己休眠,并在其完成后唤醒你。
但是,在极少数情况下,如果需要等待的时间非常短,则使用自旋锁实际上更快。这是因为执行信号函数所需的时间;如果自旋的时间小于执行信号的时间,则自旋是可取的。因此,在这种情况下,它可能会有益,并且实际上可以提高性能,但这绝对不是最常见的情况。

当等待时间短而不是非常短时,while (var == false) Thread.yield();是否适用?或者在自旋时没有(好的)情况可以使用Thread.yield()吗? - vanOekel

3
旋转等待是指您不断等待条件成立。相反的是等待信号(例如通过notify()和wait()进行线程中断)。
等待有两种方式,第一种是半主动的(sleep / yield),第二种是主动的(忙等待)。
在忙等待中,程序使用特殊操作码(如HLT或NOP或其他耗时操作)积极地空闲。其他人只是使用while循环检查条件是否成立。
Java框架提供Thread.sleep、Thread.yield和LockSupport.parkXXX()方法,让线程交出CPU。Sleep等待特定时间,但即使指定了纳秒,也始终需要超过一毫秒。与LockSupport.parkNanos(1)相同。Thread.yield允许我的示例系统(win7 + i5移动版)具有100ns的分辨率。
yield的问题在于它的工作方式。如果系统完全被利用,yield在我的测试场景中可以花费高达800ms的时间(100个工作线程都无限制地计数(a+=a;))。由于yield释放CPU并将线程添加到其优先级组内所有线程的末尾,因此yield不稳定,除非CPU没有被某种程度地利用。
繁忙等待将阻止CPU(核心)多个毫秒。
Java Framework(检查Condition类实现)在少于1000ns(1微秒)的区间内使用活动(忙碌)等待。在我的系统上,System.nanoTime的平均调用时间为160ns,因此繁忙等待就像检查条件花费160ns的纳秒并重复。
因此,Java的并发框架(队列等)有一种等待小于一微秒旋转,并以N粒度(其中N是用于检查时间约束并等待一毫秒或更长时间的纳秒数)进入等待周期。对于我的当前系统)。
因此,主动忙等待增加了利用率,但有助于系统的整体响应能力。
在烧CPU时间时,应使用特殊指令,降低执行耗时操作的核心功耗。

0

忙碌自旋就是循环等待线程完成。例如,您有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);
    }

我们可以使用CountDownLatch或CyclicBarrier,甚至可以使用join方法来实现这一目标。您能告诉我为什么这种忙等待的方式比JDK提供的上述同步工具更好吗? - Bravo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接