【发布时间】:2011-06-02 19:47:12
【问题描述】:
我正在使用 BlockingQueue:s(尝试 ArrayBlockingQueue 和 LinkedBlockingQueue)在我当前正在处理的应用程序中的不同线程之间传递对象。性能和延迟在这个应用程序中相对重要,所以我很好奇使用 BlockingQueue 在两个线程之间传递对象需要多少时间。为了衡量这一点,我编写了一个带有两个线程(一个消费者和一个生产者)的简单程序,我让生产者将时间戳(使用 System.nanoTime() 获取)传递给消费者,请参见下面的代码。
我记得在某个论坛的某个地方读到过,其他人尝试过这个过程大约需要 10 微秒(不知道在什么操作系统和硬件上),所以当它花费大约 30 微秒时我并不感到惊讶我在我的 Windows 7 机器上(英特尔 E7500 核心 2 双核 CPU,2.93GHz),同时在后台运行许多其他应用程序。然而,当我在我们更快的 Linux 服务器(两个 Intel X5677 3.46GHz 四核 CPU,运行内核为 2.6.26-2-amd64 的 Debian 5)上进行相同的测试时,我感到非常惊讶。我预计延迟会低于我的 windows 盒子,但相反它要高得多 - ~75 - 100 微秒!两项测试均使用 Sun 的 Hotspot JVM 版本 1.6.0-23 完成。
有没有其他人在 Linux 上做过类似的测试并得到类似的结果?或者有谁知道为什么它在 Linux 上慢得多(硬件更好),难道是线程切换在 Linux 上比 Windows 慢得多吗?如果是这样的话,Windows 似乎更适合某种应用程序。非常感谢任何帮助我理解相对较高的数字的帮助。
编辑:
在 DaveC 发表评论后,我还做了一个测试,我将 JVM(在 Linux 机器上)限制为单个内核(即所有线程在同一个内核上运行)。这极大地改变了结果——延迟降至 20 微秒以下,即优于 Windows 机器上的结果。我还做了一些测试,我将生产者线程限制在一个内核上,将消费者线程限制在另一个内核上(尝试将它们都放在同一个套接字和不同的套接字上),但这似乎没有帮助 - 延迟仍然是 ~75微秒。顺便说一句,这个测试应用程序几乎是我在执行测试时在机器上运行的所有内容。
有谁知道这些结果是否有意义?如果生产者和消费者在不同的内核上运行,它真的应该慢很多吗?任何意见都非常感谢。
再次编辑(1 月 6 日):
我尝试了对代码和运行环境的不同更改:
我将 Linux 内核升级到 2.6.36.2(从 2.6.26.2)。内核升级后,测量时间从升级前的 75-100 变为 60 微秒,变化非常小。为生产者和消费者线程设置 CPU 亲和性没有任何效果,除非将它们限制在同一个核心。在同一内核上运行时,测得的延迟为 13 微秒。
在原始代码中,我让生产者在每次迭代之间休眠 1 秒,以便给消费者足够的时间来计算经过的时间并将其打印到控制台。如果我删除对 Thread.sleep () 的调用,而是让生产者和消费者在每次迭代中都调用 barrier.await() (消费者在将经过的时间打印到控制台后调用它),测量的延迟从60 微秒到 10 微秒以下。如果在同一个内核上运行线程,延迟会低于 1 微秒。谁能解释为什么这会显着减少延迟?我的第一个猜测是,更改的效果是生产者在消费者调用 queue.take() 之前调用 queue.put(),因此消费者永远不必阻塞,但是在使用了 ArrayBlockingQueue 的修改版本之后,我发现这个猜测是错误的——消费者实际上阻止了。如果您有其他猜测,请告诉我。 (顺便说一句,如果我让生产者同时调用 Thread.sleep() 和 barrier.await(),延迟保持在 60 微秒)。
我还尝试了另一种方法——我没有调用 queue.take(),而是调用 queue.poll(),超时时间为 100 微秒。这将平均延迟降低到 10 微秒以下,但 CPU 密集度当然要高得多(但 CPU 密集度可能比忙等待时要少?)。
再次编辑(1 月 10 日) - 问题已解决:
ninjalj 建议约 60 微秒的延迟是由于 CPU 必须从更深的睡眠状态中唤醒 - 他完全正确!在 BIOS 中禁用 C 状态后,延迟减少到
...
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CyclicBarrier;
public class QueueTest {
ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue<Long>(10);
Thread consumerThread;
CyclicBarrier barrier = new CyclicBarrier(2);
static final int RUNS = 500000;
volatile int sleep = 1000;
public void start() {
consumerThread = new Thread(new Runnable() {
@Override
public void run() {
try {
barrier.await();
for(int i = 0; i < RUNS; i++) {
consume();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
consumerThread.start();
try {
barrier.await();
} catch (Exception e) { e.printStackTrace(); }
for(int i = 0; i < RUNS; i++) {
try {
if(sleep > 0)
Thread.sleep(sleep);
produce();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void produce() {
try {
queue.put(System.nanoTime());
} catch (InterruptedException e) {
}
}
public void consume() {
try {
long t = queue.take();
long now = System.nanoTime();
long time = (now - t) / 1000; // Divide by 1000 to get result in microseconds
if(sleep > 0) {
System.out.println("Time: " + time);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
QueueTest test = new QueueTest();
System.out.println("Starting...");
// Run first once, ignoring results
test.sleep = 0;
test.start();
// Run again, printing the results
System.out.println("Starting again...");
test.sleep = 1000;
test.start();
}
}
【问题讨论】:
-
你试过在限制 jvm 只使用一个 cpu 的 linux 机器上进行测试吗?可能有助于确定时间的去向
-
有趣 - 我试图通过使用命令 'taskset 0x00000001 java QueueTest' 启动应用程序来将其限制在特定的 CPU 上,延迟从大约 75-100 减少到 ~20 微秒!我不确定我是否理解这种方式......
-
@Johan:这些时候,您在多次迭代中报告相同吗?CyclicBarrier 用于协调处理独立任务的线程。您的任务虽然不是独立的。您有生产者和消费者都在等待屏障然后(一旦两个线程都到达障碍点)它们基本上开始在阻塞队列上同步。您可以看到各种调度组合的交错报告各种延迟。
-
是的,我打印出每次迭代,即使结果不同,范围是 75-100 微秒。我首先以不同的方式使用了 CyclicBarrier,但在上面的代码中它并不是真正必要的(长睡眠类型确保生产者不会获取时间戳并尝试在消费者准备好之前将其放入队列,至少在第一次迭代之后不会)
-
使用 Sun JVM 1.6.0-21(32 位)在 RHEL 5.5 双核 E6750 2.66G GHz 上运行此程序需要 18-25 微秒。你在这两种情况下都运行 64 位 jvm 吗?
标签: java linux multithreading latency