Task#call()方法在Task执行前被调用



根据文档,Task#call()是"在Task执行时调用"。考虑下面的程序:

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.stage.Stage;
public class TestTask extends Application {
Long start;
public void start(Stage stage) {
start = System.currentTimeMillis();
new Thread(new Taskus()).start(); 
}
public static void main(String[] args) {
launch();
}
class Taskus extends Task<Void> {
public Taskus() {
stateProperty().addListener((obs, oldValue, newValue) -> {
try {
System.out.println(newValue + " at " + (System.currentTimeMillis()-start));
} catch (Exception e) {
e.printStackTrace();
}
});
}
public Void call() throws InterruptedException {
for (int i = 0; i < 10000; i++) {
// Could be a lot longer.
}
System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start));
Thread.sleep(3000);
return null;
}
}
}

执行这个程序得到以下输出:

Some code already executed. after 5 milliseconds
SCHEDULED after 5 milliseconds
RUNNING after 7 milliseconds
SUCCEEDED after 3005 milliseconds

为什么在调度任务之前调用call()方法?这对我来说毫无意义。在我第一次看到这个问题的任务中,我的任务在进入SCHEDULED状态之前执行了几秒钟。如果我想给用户一些关于状态的反馈,并且在任务执行几秒钟之前什么都没有发生,该怎么办?

为什么在调度任务之前调用call()方法?

TLDR;版本不是的。它只是在您得到通知它已被调度之前被调用。


您有两个线程在运行,基本上是独立的:您显式创建的线程和FX应用程序线程。当启动应用程序线程时,它将在该线程上调用Taskus.call()。但是,对任务属性的更改是通过调用Platform.runLater(...)在FX应用线程上进行的。

因此,当您在线程上调用start()时,在幕后发生以下操作:

  1. 新线程启动
  2. 在该线程中,调用Task中的内部call()方法。方法:
  3. 在FX应用线程上调度一个可运行程序,将任务的stateProperty更改为SCHEDULED
  4. 在FX应用线程上调度一个可运行程序,将任务的stateProperty更改为RUNNING
  5. 调用call方法

当FX应用程序线程接收到将任务状态从READY更改为SCHEDULED,然后从SCHEDULED更改为RUNNING的可运行程序时,它会影响这些更改并通知任何侦听器。由于这与call方法中的代码在不同的线程上,因此call方法中的代码和stateProperty侦听器中的代码之间没有"happens-before"关系。换句话说,谁也不能保证哪一个会先发生。特别是,如果FX应用线程已经在忙着做一些事情(渲染UI,处理用户输入,处理传递给Platform.runLater(...)的其他Runnable,等等),它将在对任务的stateProperty进行更改之前完成这些。

可以保证的是,在call方法被调用之前,对SCHEDULEDRUNNING的更改将在FX Application线程上调度(但不一定执行),并且对SCHEDULED的更改将在执行RUNNING之前执行。

这里有一个类比。假设我接受客户编写软件的请求。把我的工作流想象成后台线程。假设我有一个行政助理为我与客户沟通。可以把她的工作流看作是FX Application线程。因此,当我收到客户的请求时,我告诉我的管理助理给客户发电子邮件,通知他们我收到了请求(SCHEDULED)。我的行政助理尽职尽责地把它列入了她的"待办事项"清单。过了一会儿,我让我的行政助理给客户发邮件,告诉他们我已经开始着手他们的项目(RUNNING),她把这件事加到了她的"待办事项"清单上。然后我开始做这个项目。我在这个项目上做了一些工作,然后在Twitter上发布了一条tweet(你的System.out.println("Some code already executed"))。"为xxx做一个项目,真的很有趣!"根据我的助理的"待办事项"列表上已有的事项数量,完全有可能在她向客户发送电子邮件之前出现tweet,因此完全有可能客户看到我已经开始在项目上工作,然后看到电子邮件说工作已被安排,即使从我的工作流程的角度来看,一切都是以正确的顺序发生的。

这通常是你想要的:状态属性被设计用来更新UI,所以它必须在FX应用线程上运行。因为你在一个不同的线程上运行你的任务,你可能希望它只是这样做:在不同的执行线程中运行。

在我看来,在调用方法实际开始执行之后,对调度状态的更改不太可能被观察到大量的时间(多于一个帧渲染脉冲,通常是1/60秒):如果发生这种情况,您可能会阻塞FX Application线程以防止它看到这些更改。在您的示例中,时间延迟显然是最小的(小于一毫秒)。

如果您想在任务启动时执行某些操作,但不关心在哪个线程上执行,只需在调用方法的开头执行即可。(根据上面的类比,这就相当于我给客户发电子邮件,而不是让我的助手去做。)

如果您真的需要调用方法中的代码在FX应用程序线程上发生一些用户通知之后发生,您需要使用以下模式:

public class Taskus extends Task<Void> {
@Override
public Void call() throws Exception {
FutureTask<Void> uiUpdate = new FutureTask<Void>(() -> {
System.out.println("Task has started");
// do some UI update here...
return null ;
});
Platform.runLater(uiUpdate);
// wait for update:
uiUpdate.get();
for (int i = 0; i < 10000; i++) {
// any VM implementation worth using is going 
// to ignore this loop, by the way...
}
System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start));
Thread.sleep(3000);
return null ;
}
}

在这个例子中,你可以保证在看到"一些代码已经执行"之前看到"Task已经开始"。此外,由于显示"任务已经启动"方法发生在与SCHEDULEDRUNNING状态变化相同的线程(FX Application线程)上,并且由于显示"任务已经启动"消息是在这些状态变化之后安排的,因此您可以保证在看到"任务已经启动"消息之前看到到SCHEDULEDRUNNING的转换。(就这个类比而言,这就好比我让我的助理发送邮件,然后在我知道她已经发送邮件之前不开始任何工作。)

还需要注意的是,如果您替换了对

的原始调用
System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start));

Platform.runLater(() -> 
System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start)));

那么您也可以保证按照您期望的顺序看到调用:

在5毫秒后调度7毫秒后运行一些代码已经执行。8毫秒后3008毫秒后成功

最后一个版本相当于我让助手帮我发推文的类比。

最新更新