为什么使用阻塞而不是循环?

6
以下代码为什么被认为是不良实践的原因有哪些?
  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,但我想知道是否有多个原因,为什么不应该编写这样的代码。

6个回答

6

在事件被处理之前,它会施加平均延迟50毫秒,并且当没有事件需要处理时,每秒唤醒10次。如果这些事情都不是特别重要的话,那么这只是一种不太优雅的方式。


只是好奇,这不是等待函数内部的工作方式吗?(当然,不包括异常。) - user541686
1
通常情况下不会。通常线程会被放置在“等待队列”中,不会分配任何处理器时间。当某些事件发生并唤醒它们时,它们将被放回运行队列中以便进行调度。这意味着,如果任何时候只有两三个线程处于活动状态,那么即使有一百万个休眠线程也不会影响性能。 - templatetypedef
那么操作系统如何确定线程是否应在特定时间片唤醒呢?它不应该在每个时间片循环中检查线程的状态吗? - user541686
@Mehrdad 操作系统不会找到它们是否应该被唤醒,事件本身会唤醒它们。这就像每隔五分钟起床看邮件是否到达和邮递员敲门的区别一样。上面的代码每秒钟唤醒十次以查看事件是否发生。正确的方法是让事件自己在发生时唤醒线程。 - David Schwartz
@DavidSchwartz:事件并不是神奇的。它是操作系统本身管理事件,因此是操作系统唤醒线程。那么,如果不是通过轮询,它在内部是如何工作的呢?是通过中断吗?(哪个中断?) - user541686
显示剩余8条评论

1

有很多不做这件事的理由。首先,正如您所指出的那样,这意味着事件发生后线程应该响应和实际响应时间之间可能会有很大的延迟,因为线程可能正在休眠。其次,由于任何系统只有那么多不同的处理器,如果您不得不不断地将重要的线程从处理器中踢出,以便它们可以再次告诉线程去休眠,您将减少系统完成的总有用工作量并增加系统的功耗(这在手机或嵌入式设备等系统中很重要)。


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纳秒的时间,这并不是一个长时间的延迟。


它可能会不必要地等待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);
   }
});

正如您所看到的,您不需要直接使用队列或线程。您只需要告诉线程池要做什么。


0

你同时也给你的类引入了竞态条件。如果你使用的是阻塞队列而不是普通列表 - 线程会一直阻塞,直到列表中有新条目出现。在你的情况下,第二个线程可以在你的工作线程睡眠时向列表中放置和获取元素,而你甚至都没有注意到。


0

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

抢占式调度:

在CPU级别上,中断是关键。操作系统在发生导致其代码被输入的中断之前什么也不做。请注意,在操作系统术语中,中断有两种类型 -“真实”的硬件中断会导致驱动程序运行,“软件中断”-这些是来自已运行线程的操作系统系统调用,可能会导致正在运行的线程集合发生变化。按键、鼠标移动、网络卡、磁盘、页面错误都会生成硬件中断。wait和signal函数以及sleep()属于第二类。当硬件中断导致驱动程序运行时,驱动程序执行其设计的任何硬件管理。如果驱动程序需要向操作系统发出信号,表明某个线程需要运行(例如,磁盘缓冲区现在已满并需要处理),操作系统提供了一个条目机制,驱动程序可以调用该条目机制,而不是直接执行中断返回本身(重要!)。

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

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

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

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

重要点:

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

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

3)抢占式调度和分派确实会产生一些同步等问题,这些问题在这些论坛上经常讨论。好处是线程对硬件中断有快速响应。如果没有这个功能,你在电脑上运行的所有高性能应用程序-视频流媒体、快速网络等都几乎不可能。

4)操作系统计时器只是一组可以更改运行线程集合的中断之一。 '时间片'(呃——我讨厌那个术语)只有在计算机超载时才会在就绪线程之间进行切换,即就绪线程的数量大于可用于运行它们的 CPU 核数。如果任何试图解释操作系统调度的文本在“中断”之前提到了“时间片”,那么它很可能会造成更多的混乱而不是解释。计时器中断之所以“特殊”,是因为许多系统调用具有超时设置来支持它们的主要功能(好吧,对于 sleep() 函数来说,超时就是其主要功能 :))。


0

除了其他答案之外,如果您有多个线程从队列中删除项目,则还会出现竞争条件:

  1. 队列为空
  2. 线程A将元素放入队列中
  3. 线程B检查队列是否为空;它不是
  4. 线程C检查队列是否为空;它不是
  5. 线程B从队列中取出;成功
  6. 线程C从队列中取出;失败

您可以通过在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


你是不是想说 remove(0)?我猜你不想忽略第一个元素。 - Peter Lawrey
在您的描述中,您提到了一个“队列”,但在您的代码中,您使用了一个“列表”。这可能会让人感到困惑。您可以使用Queue.remove();顺便说一句,LinkedList也是一个队列。 - Peter Lawrey
OP提到了someList(而不是someQueuesomeLinkedList),但是提到了BlockingQueue,所以我在我的答案中保持了这种二分法。 - yshavit
在这种情况下,我会尽量在代码和答案中一致地引用“列表”。 - Peter Lawrey

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