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在这种情况下我会使用 a ,但我想知道是否有多个不应该编写此类代码的原因。

4

6 回答 6

6

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

于 2012-01-12T07:13:28.517 回答
1

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

于 2012-01-12T07:14:05.093 回答
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);
   }
});

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

于 2012-01-12T08:18:29.983 回答
0

您还向您的班级介绍了比赛条件。如果您使用的是阻塞队列而不是普通列表 - 线程将阻塞,直到列表中有新条目。在您的情况下,第二个线程可以在您的工作线程处于睡眠状态时从列表中放置和获取一个元素,您甚至不会注意到。

于 2012-01-12T07:24:26.010 回答
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.

于 2012-01-12T07:27:11.063 回答
0

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

抢先调度/调度:

在 CPU 级别,中断是关键。在发生导致其代​​码被输入的中断之前,操作系统什么都不做。请注意,在操作系统术语中,中断有两种形式 - 导致驱动程序运行的“真实”硬件中断和“软件中断”——这些是来自已运行线程的操作系统系统调用,可能会导致一组正在运行的线程改变。按键、鼠标移动、网卡、磁盘、页面错误都会产生硬件中断。等待和信号函数以及 sleep() 属于第二类。当硬件中断导致驱动程序运行时,驱动程序执行它设计的任何硬件管理。如果驱动程序需要通知操作系统某个线程需要运行,(也许磁盘缓冲区现在已满并需要处理),

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

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

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

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

要点:

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

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

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

4) The OS timer is just one of a large set of interrupts that can change the set of running threads. 'Time-slicing', (ugh - I hate that term), between ready threads only occurs when the computer is overloaded, ie. the set of ready threads is larger than the number of CPU cores available to run them. If any text purporting to explain OS scheduling mentions 'time-slicing' before 'interrupts', it is likely to cause more confusion than explanation. The timer interrupt is only 'special' in that many system calls have timeouts to back up their primary function, (OK, for sleep(), the timeout IS the primary function:).

于 2012-01-12T11:20:37.317 回答