为什么阻止而不是循环?

时间:2012-01-12 07:10:49

标签: java multithreading concurrency busy-loop

为什么编写以下代码被视为不良做法有什么原因?

  while (someList.isEmpty()) {
    try {
      Thread.currentThread().sleep(100);
    }
    catch (Exception e) {}
  }
  // Do something to the list as soon as some thread adds an element to it.

对我而言,选择任意值进行睡眠并不是一种好习惯,在这种情况下我会使用BlockingQueue,但我想知道为什么一个人不应该使用{{1}}写这样的代码。

6 个答案:

答案 0 :(得分:6)

它会在事件发生之前平均延迟50毫秒,并且当没有事件要处理时它会每秒唤醒10次。如果这些事情都不重要,那么它就是不优雅的。

答案 1 :(得分:1)

有很多理由不这样做。首先,正如您所指出的,这意味着在线程应响应的事件发生的时间与实际响应时间之间可能存在较大的延迟,因为线程可能正在休眠。其次,由于任何系统只有这么多不同的处理器,如果你不得不继续从处理器中踢出重要的线程,这样他们就可以告诉线程再次进入休眠状态,你就减少了系统完成的有用工作总量。并增加系统的功耗(在电话或嵌入式设备等系统中很重要)。

答案 2 :(得分:1)

循环是不该做的一个很好的例子。 ;)


Thread.currentThread().sleep(100);

不需要获取currentThread(),因为这是一个静态方法。它与

相同
Thread.sleep(100);

catch (Exception e) {}

这是非常糟糕的做法。如此糟糕,我不建议你把它放在例子中,因为有人可能会复制代码。通过打印和阅读给出的例外情况,可以解决该论坛上的很多问题。


You don't need to busy wait here. esp. when you expect to be waiting for such a long time.  Busy waiting can make sense if you expect to be waiting a very very short amount of time. e.g.

// From AtomicInteger
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}

正如你所看到的,这个循环需要多次出现并且指数上不太可能多次出现的情况应该是非常罕见的。 (在实际应用中,而不是微基准测试)这个循环可能短至10 ns,这不是一个长时间的延迟。


它可能会不必要地等待99毫秒。假设生产者在1毫秒后添加了一个条目,它已经等了很长时间。

解决方案更简单,更清晰。

BlockingQueue<E> queue = 

E e = queue.take(); // blocks until an element is ready.

列表/队列只会在另一个线程中更改,而用于管理线程和队列的更简单的模型是使用ExecutorService

ExecutorService es =

final E e = 
es.submit(new Runnable() {
   public void run() {
       doSomethingWith(e);
   }
});

如您所见,您不需要直接使用队列或线程。你只需要说出你想要线程池做什么。

答案 3 :(得分:0)

你还向你的班级介绍种族条件。如果您使用阻塞队列而不是正常列表 - 线程将阻塞,直到列表中有新条目。在你的情况下,第二个线程可以在你的工作线程处于休眠状态时放入并从列表中获取一个元素,你甚至都不会注意到。

答案 4 :(得分:0)

要添加到其他答案,如果您有多个线程从队列中删除项目,您也会遇到竞争条件:

  1. 队列为空
  2. 线程A将一个元素放入队列
  3. 线程B检查队列是否为空;它不是
  4. 线程C检查队列是否为空;它不是
  5. 线程B从队列中获取;成功
  6. 线程C从队列中获取;失败
  7. 您可以通过原子方式(在synchronized块内)检查队列是否为空,如果不是,则从中获取一个元素;现在你的循环看起来只是一个丑陋的头发:

    T item;
    while ( (item = tryTake(someList)) == null) {
        try {
            Thread.currentThread().sleep(100);
        }
        catch (InterruptedException e) {
            // it's almost never a good idea to ignore these; need to handle somehow
        }
    }
    // Do something with the item
    
    synchronized private T tryTake(List<? extends T> from) {
        if (from.isEmpty()) return null;
        T result = from.remove(0);
        assert result != null : "list may not contain nulls, which is unfortunate"
        return result;
    }
    

    您刚刚使用了BlockingQueue

答案 5 :(得分:0)

我无法直接添加David,templatetypedef等提供的优秀答案 - 如果您想避免线程间通信延迟和资源浪费,请不要使用sleep()循环进行线程间通信。

抢先式调度/调度:

在CPU级别,中断是关键。在发生导致其代​​码输入的中断之前,操作系统不执行任何操作。请注意,在OS术语中,中断有两种形式 - 导致驱动程序运行的“实际”硬件中断和“软件中断” - 这些是来自已经运行的线程的OS系统调用,可能会导致一组正在运行的线程改变。键盘,鼠标移动,网卡,磁盘,页面错误都会产生硬件中断。等待和信号函数以及sleep()属于第二类。当硬件中断导致驱动程序运行时,驱动程序会执行其设计的任何硬件管理。如果驱动程序需要向操作系统发出某个线程需要运行的信号(可能是磁盘缓冲区现已满,需要处理),则操作系统提供了一种驱动程序可以调用的输入机制,而不是直接执行中断 - 回归自己,(重要!)。

上述示例中断可以使正在等待运行的线程和/或可以使正在运行的线程进入等待状态。在处理中断代码之后,OS应用其调度算法来确定在中断之前运行的线程集是否与现在应该运行的集相同。如果是,则操作系统只是中断返回,否则,操作系统必须抢占一个或多个正在运行的线程。如果操作系统需要抢占在CPU核心上运行的线程而不是处理中断的线程,则必须获得对该CPU核心的控制权。它通过“真正的”硬件中断来实现这一点 - 操作系统处理器间驱动程序设置硬件信号,硬件中断运行要被抢占的线程的核心。

当要抢占的线程进入操作系统代码时,操作系统可以保存该线程的完整上下文。一些寄存器已经通过中断条目保存到线程的堆栈中,因此保存线程的堆栈指针将有效地“保存”所有这些寄存器,但操作系统通常需要做更多,例如。可能需要刷新缓存,可能需要保存FPU状态,并且在要运行的新线程属于与要被抢占的进程不同的进程的情况下,需要交换内存管理保护寄存器。通常,OS会尽快从中断线程堆栈切换到专用OS堆栈,以避免将OS堆栈需求强加到每个线程堆栈上。

一旦保存了context / s,操作系统就可以“交换”扩展的上下文用于要运行的新线程。现在,操作系统最终可以为新线程加载堆栈指针并执行中断返回以使其新的就绪线程运行。

然后操作系统什么都不做。正在运行的线程一直运行,直到发生另一个中断(硬或软)。

重点:

1)操作系统内核应该被视为一个大的中断处理程序,它可以决定中断 - 返回一组不同于中断的线程。

2)操作系统可以控制并在必要时停止任何进程中的任何线程,无论它处于什么状态或运行的核心。

3)抢占式调度和调度确实会生成在这些论坛上发布的所有同步等问题。最大的好处是在线程级别对硬中断的快速响应。如果没有这个,你在PC上运行的所有高性能应用程序 - 视频流,快速网络等,几乎是不可能的。

4)OS定时器只是可以更改正在运行的线程集的大量中断之一。 “时间切片”,(呃 - 我讨厌那个术语),准备好的线程之间只发生在计算机过载时,即。准备好的线程集大于可用于运行它们的CPU核心数。如果任何声称解释操作系统调度的文本在“中断”之前提到“时间切片”,则可能会引起比解释更多的混淆。定时器中断只是“特殊”,因为许多系统调用都有超时来备份它们的主要功能,(OK,对于sleep(),超时是主要功能:)。