我知道使用忙等待不是一个好的编程实践,最好尽可能使用同步对象(wait-notify)。但如果可以牺牲CPU周期,我想知道忙等待和wait-notify哪个更快?
我认为,wait-notify将涉及到同步对象上的内部锁,并且信号可能来自内核以唤醒线程,使这种方法比忙等待慢得多,而在忙等待中,可以持续检查一个条件,直到满足条件。一旦满足条件(例如布尔值== true),线程就可以退出忙等待。根据我的理解,我认为忙等待应该更快。
如果其他人能分享他们的想法并纠正我的观点,我将不胜感激。
我知道使用忙等待不是一个好的编程实践,最好尽可能使用同步对象(wait-notify)。但如果可以牺牲CPU周期,我想知道忙等待和wait-notify哪个更快?
我认为,wait-notify将涉及到同步对象上的内部锁,并且信号可能来自内核以唤醒线程,使这种方法比忙等待慢得多,而在忙等待中,可以持续检查一个条件,直到满足条件。一旦满足条件(例如布尔值== true),线程就可以退出忙等待。根据我的理解,我认为忙等待应该更快。
如果其他人能分享他们的想法并纠正我的观点,我将不胜感激。
实验表明,如果你使用忙等待,你会比等待并通知(在我的硬件上,至少是这样)更早地看到标志。 (详细信息如下。)差异非常非常非常非常小,因此这仅适用于非常罕见的应用程序。例如股票交易应用程序,其中公司正在寻求他们可以获得的任何优势(争取将其服务器定位在距离交易所尽可能近的位置,以获得网络反馈中微秒级的改进等),可能认为这种差异是值得的。我还可以想象一些科学应用。
在绝大多数应用程序中,差异实际上没有任何区别。
但是,对于CPU来说会出现一个核心的硬长期占用:
这对于影响盒子上的其他进程和数据中心中的功耗都不好。
因此:只有在真正重要的情况下,才极度勉强地使用。
数据(非常小的样本,但代码如下):
繁忙等待:10631 12350 15278 等待并通知:87299 120964 107204 Delta:76668 108614 91926
时间以纳秒为单位。十亿分之一秒。上面的平均增量为92403ns(0.092402667毫秒,0.000092403秒)。
BusyWait.java
:
public class BusyWait {
private static class Shared {
public long setAt;
public long seenAt;
public volatile boolean flag = false;
}
public static void main(String[] args) {
final Shared shared = new Shared();
Thread notifier = new Thread(new Runnable() {
public void run() {
System.out.println("Running");
try {
Thread.sleep(500);
System.out.println("Setting flag");
shared.setAt = System.nanoTime();
shared.flag = true;
}
catch (Exception e) {
}
}
});
notifier.start();
while (!shared.flag) {
}
shared.seenAt = System.nanoTime();
System.out.println("Delay between set and seen: " + (shared.seenAt - shared.setAt));
}
}
WaitAndNotify.java
:
public class WaitAndNotify {
private static class Shared {
public long setAt;
public long seenAt;
public boolean flag = false;
}
public static void main(String[] args) {
(new WaitAndNotify()).test();
}
private void test() {
final Shared shared = new Shared();
final WaitAndNotify instance = this;
Thread notifier = new Thread(new Runnable() {
public void run() {
System.out.println("Running");
try {
Thread.sleep(500);
System.out.println("Setting flag");
shared.setAt = System.nanoTime();
shared.flag = true;
synchronized (instance) {
instance.notify();
}
}
catch (Exception e) {
}
}
});
notifier.start();
while (!shared.flag) {
try {
synchronized (this) {
wait();
}
}
catch (InterruptedException ie) {
}
}
shared.seenAt = System.nanoTime();
System.out.println("Delay between set and seen: " + (shared.seenAt - shared.setAt));
}
}
wait
和notify
调用周围进行同步。当然,在理论上,我们实际上从来没有循环(因为那需要除了我们的升旗代码之外的其他东西执行notify
),我们只进入while
的主体一次,然后永远不会重复,所以对于这个测试代码,无论哪种方式都是一样的。 - T.J. Crowder在忙等待的情况下,人们准备牺牲CPU周期以获取更快的速度。
一个关于忙等待实时低延迟应用的例子是,有一个名为LMAX Disruptor 的框架,它是为伦敦证券交易所打造的,其中一种锁定策略是忙等待,并且这就是他们使用的方式。
为了实现超高速,最好浪费CPU周期而不是浪费在等待锁通知上的时间。
您对其他内容的理解也是正确的,如果您在Google上搜索Disruptor并阅读他们的论文,您会获得更多的澄清。关于高性能和低延迟,还有太多要说的。
一个不错的博客可以参考机械同理心。
这要看情况:
情况A
如果'busy-wait'正在等待的是硬件操作(例如,将硬盘扇区读入内存):
1)硬件会执行该操作。
2)驱动程序将启动中断。
3)操作将暂停实际进程(即您的busy-wait进程),保存中断处理中覆盖的任何CPU寄存器的实际值。
4)中断将被处理,修改任何指示数据可用的标志(例如,在读取磁盘时)。
5)覆盖的寄存器将被恢复。
6)您的进程将继续进行。在下一次迭代中,它将调用循环的条件。例如,如果busy wait是:
while( !fileReady() ){
...
}
方案B
另一方面,如果繁忙方法正在等待另一个进程结束(或将任何变量设置为特定值),则繁忙等待将变慢。
1)繁忙方法将在CPU上运行。由于其他进程未运行,条件无法更改,因此繁忙方法将一直运行,直到CPU决定将CPU时间分配给另一个进程。
2)其他进程将运行。如果该进程花费的时间没有达到繁忙等待进程所需的条件,则返回第1步,否则继续执行第3步。
3)其他(非繁忙等待)进程仍将运行一段时间,直到CPU决定更换新进程。
4)繁忙方法将再次运行,但此时条件已满足,因此操作现在已完成。
结论:速度较慢,并且我们还会减缓整个过程。
方案 C
如果我们有与方案 B 相同的情况,但有多个核心(每个核心一个进程),该怎么办?
首先,请记住,即使您拥有具有多个核心的 CPU,您的程序可能也不允许使用超过一个核心。而且可能有一些操作系统或其他程序正在使用它们。
其次,这并不值得。请记住,进程必须进行通信,以便繁忙等待找到满足条件的情况。通常可以通过“最终”变量来完成此操作,因此每次评估条件时都需要进入同步块(您不能在进入循环之前锁定并解锁它,因为在这种情况下,另一个进程将无法更改变量)。所以你需要像这样的东西:
boolean exit = false;
while( exit==false ){
synchronize(var){
if(var = CONDITIONS_MEET)
exit=true;
}
}
//operations...
}
但是,使用等待-通知机制会更加高效(在语言层面上),不会浪费CPU周期,并且使用良好的实践方法!
结论:你正在使自己的生活变得复杂,而这样做很可能不会更快(非常非常非常不可能)。
忙等待比普通的等待通知更快。
为什么要等待?因为生产者或其他线程会执行一些工作,然后设置条件(或通知),以便您可以实际上退出繁忙/等待循环。 现在假设如果您的生产者正在执行一些重负载任务,那么通过执行繁忙等待来占用其CPU周期(主要适用于单处理器系统),这将反过来可能使您的系统整体变慢。
那么现在应该何时使用繁忙等待呢? 正如Claudio所说,它主要用于低延迟系统。 但仍不应盲目使用。当您的生产者以稳定速率生产物品时,请使用繁忙等待。 如果您的生产者以可变速率(通常由Poisson分布演示)生产物品,则应该使用wait notify。
通常,在高吞吐量和低延迟系统中,最好的权衡是进行一段时间的繁忙等待,然后进入wait()状态。 如果您的系统需要超低延迟,则可以进行许多优化之一是Busy-Wait。 但是,不应该每个线程都在繁忙等待。确保只有一些消费者约N/2个消费者正在繁忙等待,其中N是您系统中的核心数。浪费CPU周期可能会影响整体性能和响应能力。 供您参考:即使是普通的ReentrantLock及其变体也适用这些策略。即当线程调用lock.lock()时,在进入队列并等待锁被释放之前,它尝试两次获取锁。对于低延迟系统,甚至可以为特定场景定义自己的锁,在进入队列之前尝试多达10次(它们将是所谓的自旋锁变体)。
wait()/notify()
的优势在于一旦调用notify()
,等待中的线程之一将被通知并开始执行。也就是说,调用notify()
的线程不会继续执行。如果忙等待,即使第二个线程设置了第一个线程正在等待的布尔标志,第二个线程仍将执行直到其时间片段完成,然后第一个线程才开始执行。如有错误请其他人指正。 - TheLostMindnotify()
不会导致调用线程挂起,也不会立即唤醒等待的线程。相反,被通知的线程仍然需要等待通知线程先离开同步块。关于时间片问题:在多核系统中,线程可能真正并行运行!因此你的说法并不完全正确。 - isnot2bad