【问题标题】:Task#call() method invoked before task is executed在任务执行之前调用的 Task#call() 方法
【发布时间】:2016-01-14 03:45:42
【问题描述】:

根据文档,Task#call() 是“在执行任务时调用”。 考虑以下程序:

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 状态前几秒钟执行。如果我想给用户一些关于状态的反馈,而在任务已经执行几秒钟之前什么都没有发生怎么办?

【问题讨论】:

    标签: javafx concurrency task scheduled-tasks


    【解决方案1】:

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

    TLDR;版本:不是。它只是在您收到已安排好的通知之前调用。


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

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

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

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

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

    这是一个类比。假设我接受客户的请求来编写软件。将我的工作流程视为后台线程。假设我有一个管理员助理为我与客户沟通。将她的工作流程视为 FX 应用程序线程。因此,当我收到客户的请求时,我会告诉我的管理员助理给客户发送电子邮件并通知他们我收到了请求 (SCHEDULED)。我的行政助理尽职尽责地把它放在她的“待办事项”清单上。不久之后,我告诉我的管理员助理给客户发电子邮件,告诉他们我已经开始处理他们的项目 (RUNNING),她将其添加到她的“待办事项”列表中。然后我开始研究这个项目。我在这个项目上做了一些工作,然后在 Twitter 上发布了一条推文(你的 System.out.println("Some code already executed"))“为 xxx 做一个项目,这真的很有趣!”。根据我助理的“待办事项”列表中已有的事情的数量,推文很可能会在她将电子邮件发送给客户之前出现,因此客户很可能会在看到我之前就已经开始进行该项目的工作电子邮件说工作已经安排好了,尽管从我的工作流程的角度来看,一切都按正确的顺序发生。

    这通常是您想要的:status 属性旨在用于更新 UI,因此它必须在 FX 应用程序线程上运行。由于您在不同的线程上运行任务,因此您可能希望它这样做:在不同的执行线程中运行。

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

    如果你想在任务开始时做某事,但不关心你在哪个线程上做,只要在调用方法的开头就做。 (根据上面的类比,这相当于我将电子邮件发送给客户,而不是要求我的助手这样做。)

    如果您确实需要在 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 ;
        }
    }
    

    在此示例中,您一定会先看到“任务已启动”,然后再看到“一些代码已执行”。此外,由于显示“任务已启动”方法发生在与SCHEDULEDRUNNING 的状态更改相同的线程(FX 应用程序线程)上,并且由于显示“任务已启动”消息被安排在之后如果状态发生变化,则可以保证在看到“任务已开始”消息之前看到到 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 毫秒后成功

    最后一个版本相当于我要求我的助手为我发布推文。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-12-01
      • 2018-03-13
      • 1970-01-01
      • 2015-02-02
      • 1970-01-01
      • 2014-05-17
      • 1970-01-01
      相关资源
      最近更新 更多