为什么 JUnit 使用 CountDownLatch 来实现 FailOnTimeout
Why JUnit using CountDownLatch to implement FailOnTimeout
我刚开始阅读 JUnit 4.13 (https://github.com/junit-team/junit) 的代码,对 org.junit.internal.runners.statements.FailOnTimeout
的实现有点困惑:
@Override
public void evaluate() throws Throwable {
CallableStatement callable = new CallableStatement();
FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
Thread thread = new Thread(threadGroup, task, "Time-limited test");
thread.setDaemon(true);
thread.start();
callable.awaitStarted();
Throwable throwable = getResult(task, thread);
if (throwable != null) {
throw throwable;
}
}
/**
* Wait for the test task, returning the exception thrown by the test if the
* test failed, an exception indicating a timeout if the test timed out, or
* {@code null} if the test passed.
*/
private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
try {
if (timeout > 0) {
return task.get(timeout, timeUnit); // HERE limits the time
} else {
return task.get();
}
} catch (InterruptedException e) {
return e; // caller will re-throw; no need to call Thread.interrupt()
} catch (ExecutionException e) {
// test failed; have caller re-throw the exception thrown by the test
return e.getCause();
} catch (TimeoutException e) {
return createTimeoutException(thread);
}
}
其中 CallableStatement
是:
private class CallableStatement implements Callable<Throwable> {
private final CountDownLatch startLatch = new CountDownLatch(1);
public Throwable call() throws Exception {
try {
startLatch.countDown();
originalStatement.evaluate(); // HERE the test runs
} catch (Exception e) {
throw e;
} catch (Throwable e) {
return e;
}
return null;
}
public void awaitStarted() throws InterruptedException {
startLatch.await();
}
}
下面是我对代码的理解:
evaluate()
为测试方法启动一个新线程。 callable.awaitStarted()
块 evaluate()
直到 startLatch.countDown()
,然后 getResult()
次测试方法。
这是我的问题:
- 为什么
thread
(在 evaluate()
中)应该是守护线程?
- 难道
CountDownLatch
只是用来屏蔽getResult()
直到thread
被运行宁?它真的有效吗(我认为没有什么可以避免 callable.awaitStarted()
和 getResult()
之间的上下文切换)?有什么 "simpler" 方法可以做到这一点吗?
我对并发不是很熟悉。如果有人能解释这些或指出我的错误,我将不胜感激。谢谢。
关于第二个问题的更多解释:
我会将这两个线程表示为 "evaluate() thread" 和 "CallableStatement thread"。
我认为 "evaluate() thread" 在执行 callable.awaitStarted()
时被阻塞,直到 startLatch.countDown()
完成,但测试方法可能会在上下文切换回 [之前开始 运行 =59=]。 "evaluate() thread"再次唤醒后立即调用FutureTask.get()
,会阻塞"evaluate() thread",但不能保证"CallableStatement thread"紧接着被唤醒。
所以,我认为测试方法开始的时刻与调用 task.get(timeout, timeUnit)
的时刻无关。如果有很多其他线程,它们之间的时间间隔可以忽略不计。
Why thread (in evaluate()) should be a daemon thread?
如果测试有任何问题,它允许 JVM 正确退出。另见 What is Daemon thread in Java?
Is CountDownLatch just used for blocking the getResult() until thread is running? Does it really work (I thought nothing can avert a context switch between callable.awaitStarted() and getResult())? Is there any "simpler" way to do this?
没有什么可以避免上下文切换,但如果 thread
已经启动并且 运行 它最终会得到一些 CPU 关注并且 originalStatement.evaluate()
将被执行。这样做的原因是可能没有可用的底层操作系统线程,然后测试的测试执行可能会失败,尽管测试本身是正确的。还有其他方法可以做到这一点,例如Semaphore
,但是 CountDownLatch
非常有效,而且 便宜 原始。
我刚开始阅读 JUnit 4.13 (https://github.com/junit-team/junit) 的代码,对 org.junit.internal.runners.statements.FailOnTimeout
的实现有点困惑:
@Override
public void evaluate() throws Throwable {
CallableStatement callable = new CallableStatement();
FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
Thread thread = new Thread(threadGroup, task, "Time-limited test");
thread.setDaemon(true);
thread.start();
callable.awaitStarted();
Throwable throwable = getResult(task, thread);
if (throwable != null) {
throw throwable;
}
}
/**
* Wait for the test task, returning the exception thrown by the test if the
* test failed, an exception indicating a timeout if the test timed out, or
* {@code null} if the test passed.
*/
private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
try {
if (timeout > 0) {
return task.get(timeout, timeUnit); // HERE limits the time
} else {
return task.get();
}
} catch (InterruptedException e) {
return e; // caller will re-throw; no need to call Thread.interrupt()
} catch (ExecutionException e) {
// test failed; have caller re-throw the exception thrown by the test
return e.getCause();
} catch (TimeoutException e) {
return createTimeoutException(thread);
}
}
其中 CallableStatement
是:
private class CallableStatement implements Callable<Throwable> {
private final CountDownLatch startLatch = new CountDownLatch(1);
public Throwable call() throws Exception {
try {
startLatch.countDown();
originalStatement.evaluate(); // HERE the test runs
} catch (Exception e) {
throw e;
} catch (Throwable e) {
return e;
}
return null;
}
public void awaitStarted() throws InterruptedException {
startLatch.await();
}
}
下面是我对代码的理解:
evaluate()
为测试方法启动一个新线程。 callable.awaitStarted()
块 evaluate()
直到 startLatch.countDown()
,然后 getResult()
次测试方法。
这是我的问题:
- 为什么
thread
(在evaluate()
中)应该是守护线程? - 难道
CountDownLatch
只是用来屏蔽getResult()
直到thread
被运行宁?它真的有效吗(我认为没有什么可以避免callable.awaitStarted()
和getResult()
之间的上下文切换)?有什么 "simpler" 方法可以做到这一点吗?
我对并发不是很熟悉。如果有人能解释这些或指出我的错误,我将不胜感激。谢谢。
关于第二个问题的更多解释:
我会将这两个线程表示为 "evaluate() thread" 和 "CallableStatement thread"。
我认为 "evaluate() thread" 在执行 callable.awaitStarted()
时被阻塞,直到 startLatch.countDown()
完成,但测试方法可能会在上下文切换回 [之前开始 运行 =59=]。 "evaluate() thread"再次唤醒后立即调用FutureTask.get()
,会阻塞"evaluate() thread",但不能保证"CallableStatement thread"紧接着被唤醒。
所以,我认为测试方法开始的时刻与调用 task.get(timeout, timeUnit)
的时刻无关。如果有很多其他线程,它们之间的时间间隔可以忽略不计。
Why thread (in evaluate()) should be a daemon thread?
如果测试有任何问题,它允许 JVM 正确退出。另见 What is Daemon thread in Java?
Is CountDownLatch just used for blocking the getResult() until thread is running? Does it really work (I thought nothing can avert a context switch between callable.awaitStarted() and getResult())? Is there any "simpler" way to do this?
没有什么可以避免上下文切换,但如果 thread
已经启动并且 运行 它最终会得到一些 CPU 关注并且 originalStatement.evaluate()
将被执行。这样做的原因是可能没有可用的底层操作系统线程,然后测试的测试执行可能会失败,尽管测试本身是正确的。还有其他方法可以做到这一点,例如Semaphore
,但是 CountDownLatch
非常有效,而且 便宜 原始。