如果启动了新的任务实例,如何取消任务?

How to cancel a task if new instance of task is started?

我的应用程序包含一个 ListView,它会在每次选择项目时启动后台任务。后台任务会在成功完成后更新 UI 上的信息。

但是,当用户快速单击一个又一个项目时,所有这些任务都会继续,最后一个任务将完成 "wins" 并更新 UI,无论最后选择了哪个项目。

我需要以某种方式确保此任务在任何给定时间只有一个实例 运行,因此在开始新任务之前取消所有先前的任务。

这是一个演示问题的 MCVE:

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class taskRace  extends Application {

    private final ListView<String> listView = new ListView<>();
    private final Label label = new Label("Nothing selected");
    private String labelValue;

    public static void main(String[] args) {

        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {

        // Simple UI
        VBox root = new VBox(5);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));
        root.getChildren().addAll(listView, label);

        // Populate the ListView
        listView.getItems().addAll(
                "One", "Two", "Three", "Four", "Five"
        );

        // Add listener to the ListView to start the task whenever an item is selected
        listView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {

            if (newValue != null) {

                // Create the background task
                Task task = new Task() {
                    @Override
                    protected Object call() throws Exception {

                        String selectedItem = listView.getSelectionModel().getSelectedItem();

                        // Do long-running task (takes random time)
                        long waitTime = (long)(Math.random() * 15000);
                        System.out.println("Waiting " + waitTime);
                        Thread.sleep(waitTime);
                        labelValue = "You have selected item: " + selectedItem ;
                        return null;
                    }
                };

                // Update the label when the task is completed
                task.setOnSucceeded(event ->{
                    label.setText(labelValue);
                });

                new Thread(task).start();
            }

        });

        stage.setScene(new Scene(root));
        stage.show();

    }
}

以随机顺序单击多个项目时,结果不可预测。我需要更新标签以显示上次执行的 Task 的结果。

我是否需要以某种方式安排任务或将它们添加到服务以取消所有先前的任务?

编辑:

在我的实际应用程序中,用户从 ListView 中选择一个项目,后台任务读取数据库(一个复杂的 SELECT 语句)以获取与该项目相关的所有信息。这些详细信息随后会显示在应用程序中。

发生的问题是,当用户选择一个项目但更改了他们的选择时,应用程序中显示的返回数据可能是第一个选择的项目,即使现在选择了一个完全不同的项目。

可以完全丢弃从第一个(即不需要的)选择返回的任何数据。

调用 javafx.concurrent.Service.cancel() 将取消任何当前 运行 Task,使其抛出 InterruptedException.

Cancels any currently running Task, if any. The state will be set to CANCELLED.
~ Service (JavaFX 8) - cancel ~

如果需要额外的清理,可以提供 Task.onCancelled() 事件处理程序。

演示class

package com.changehealthcare.pid;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ThreadFun {
    public static ExecutorService threadExecutor = Executors.newFixedThreadPool(1);
    public static ServiceThread serviceThreads = new ServiceThread();
    public static Future futureService;

    public static void main(String[] args) throws Exception {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String line = "";

           while (line.equalsIgnoreCase("quit") == false) {
               line = in.readLine();
               //do something
               if (futureService != null) {
                   System.out.println("try to cancel");
                   futureService.cancel(true);
               }
               futureService = threadExecutor.submit(serviceThreads);
           }

           in.close();

    }
}

ServiceThread class

package com.changehealthcare.pid;

public class ServiceThread implements Runnable {

    @Override
    public void run() {
        System.out.println("starting the service");

        boolean isResult = this.doStuff();

        if (isResult) {
            this.displayStuff();
        }
        System.out.println("done");
    }

    private boolean doStuff() {
        try {
            for (int i=0; i<=10; i++) {
                System.out.println(i);
                Thread.sleep(1000);
            }

            return true;
        }
        catch (InterruptedException e) {
            System.out.println("cancelled");
            return false;
        }
    }

    private void displayStuff() {
        System.out.println("display done");
    }
}

说明 演示应用程序充当 System.in(键盘)的侦听器。如果按下任何字符(使用 Enter),它将取消任何当前线程(如果存在)并提交一个新线程。

服务线程演示有一个 运行() 方法,它调用 doStuff() 和 displayStuff()。您可以为 displayStuff 注入 UI 部分。在 doStuff() 中,它正在捕获 InterupptedException,如果发生该异常,我们不执行 displayStuff()。

请检查并查看它是否适合您的情况。我们可以多锻炼。

最简单的代码可以满足您的需求:

class SingleTaskRunner {

    private Task<?> lastTask = null; 

    void runTask(Task<?> task) {
        registerTask(task);
        new Thread(task).start();
    }

    private synchronized void registerTask(Task<?> task) {
        if (lastTask != null) {
            lastTask.cancel(true);
        }
        lastTask = task;
    }
}

请注意,它与 非常相似,只是有同步,没有所有这些额外的演示代码。

您评论说您对上述答案如何允许您取消之前的所有任务感到困惑,但事实是 - 当您正确同步时(如 registerTask) - 您只需要存储最后一个任务,因为所有之前的任务都已经被取消(通过相同的 registerTask 方法,由于同步 - 永远不会同时执行)。据我所知,在已经完成的 Task 上调用 cancel 是空操作。

此外,在 new Thread(task).start() 上使用 ExecutorService(就像 Nghia Do 提出的那样)也是一个好主意,因为 Thread creation is expensive.


编辑:抱歉,我之前没有尝试过您的 MCVE。既然我有,但我无法重现该行为,我注意到您的 MCVE 中存在以下缺陷:

  1. 您在内部使用 Thread.sleep() to simulate calculations, but calling Future.cancel() when inside Thread.sleep() terminates the task immediately because Future.cancel() calls Thread.interrupt(),并且 Thread.sleep() 检查中断标志,您很可能不会在实际代码中这样做。

  2. 您正在将计算结果分配给私有字段。由于(如上所述)计算 was 在您的原始 MCVE 中立即中断,并且可能 wasn't 在您的真实代码中中断,在您的真实代码字段的值可能会被已取消的任务覆盖。

  3. 我相信在您的实际代码中,您所做的可能不仅仅是将结果分配给字段。在原始 MCVE 中,即使在用循环替换 Thread.sleep() 之后,我也无法重现该行为。这样做的原因是 onSucceeded 处理程序确实 而不是 被调用,当任务被取消时(这很好)。不过,我相信,在你的真实代码中,你可能在 Task.call() 中做了比你在 MCVE 中展示的更重要的事情(比如一些 GUI 更新),否则,据我所知,你不会'遇到过原问题。

这是对您的 MCVE 的修改,应该不会表现出原来的问题。

编辑:我进一步修改了 MCVE 以打印取消信息并打印事件时间。但是,此代码不会重现 OP 所写的行为。

public class TaskRace extends Application {

    private final ListView<String> listView = new ListView<>();
    private final Label label = new Label("Nothing selected");
    private final SingleTaskRunner runner = new SingleTaskRunner();

    private final long startMillis = System.currentTimeMillis();

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {

        // Simple UI
        VBox root = new VBox(5);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));
        root.getChildren().addAll(listView, label);

        // Populate the ListView
        listView.getItems().addAll(
                "One", "Two", "Three", "Four", "Five"
        );


        // Add listener to the ListView to start the task whenever an item is selected
        listView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {

            if (newValue != null) {
                // Create the background task
                MyTask task = new MyTask();

                // Update the label when the task is completed
                task.setOnSucceeded(event -> {
                    label.setText(task.getValue());
                    println("Assigned " + task.selectedItem);
                });
                task.setOnCancelled(event -> println("Cancelled " + task.selectedItem));

                runner.runTask(task);
            }

        });

        stage.setScene(new Scene(root));
        stage.show();

    }

    private void println(String string) {
        System.out.format("%5.2fs: %s%n", 0.001 * (System.currentTimeMillis() - startMillis), string);
    }

    private class MyTask extends Task<String> {

        final String selectedItem = listView.getSelectionModel().getSelectedItem();

        @Override
        protected String call() {
            int ms = new Random().nextInt(10000);
            println(String.format("Will return %s in %.2fs", selectedItem, 0.001 * ms));

            // Do long-running task (takes random time)
            long limitMillis = System.currentTimeMillis() + ms;
            while (System.currentTimeMillis() < limitMillis) {
            }

            println("Returned " + selectedItem);
            return "You have selected item: " + selectedItem;
        }
    }
}

您的要求,如 mentions, seem like a perfect reason for using a ServiceService 允许您在任何给定时间以可重用1 的方式运行 一个 Task。当您通过 Service.cancel() 取消 Service 时,它会取消基础 TaskService 还为您Task 跟踪自己的,因此您无需将它们保存在某个列表中。

使用您的 MVCE,您想做的是创建一个 Service 来包装您的 Task。每次用户在 ListView 中选择一个新项目时,您将取消 Service,更新必要的状态,然后重新启动 Service。然后您将使用 Service.setOnSucceeded 回调将结果设置为 Label。这保证只有最后一次成功执行才会 returned 给你。即使之前取消 Task 仍然是 return 结果 Service 将忽略它们。

您也不必担心外部同步(至少在您的 MVCE 中)。所有处理启动、取消和观察 Service 的操作都发生在 FX 线程上。在 FX 线程上执行的唯一代码(如下所示)not 将在 Task.call() 内(好吧,class 实例化时立即分配的字段我相信这发生在 JavaFX-Launcher 线程上。

这是您的 MVCE 的修改版本,使用 Service:

import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    private final ListView<String> listView = new ListView<>();
    private final Label label = new Label("Nothing selected");

    private final QueryService service = new QueryService();

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        service.setOnSucceeded(wse -> {
            label.setText(service.getValue());
            service.reset();
        });
        service.setOnFailed(wse -> {
            // you could also show an Alert to the user here
            service.getException().printStackTrace();
            service.reset();
        });

        // Simple UI
        VBox root = new VBox(5);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));
        root.getChildren().addAll(listView, label);

        // Populate the ListView
        listView.getItems().addAll(
                "One", "Two", "Three", "Four", "Five"
        );

        listView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {
            if (service.isRunning()) {
                service.cancel();
                service.reset();
            }
            service.setSelected(newValue);
            service.start();
        });

        stage.setScene(new Scene(root));
        stage.show();

    }

    private static class QueryService extends Service<String> {

        // Field representing a JavaFX property
        private String selected;

        private void setSelected(String selected) {
            this.selected = selected;
        }

        @Override
        protected Task<String> createTask() {
            return new Task<>() {

                // Task state should be immutable/encapsulated
                private final String selectedCopy = selected;

                @Override
                protected String call() throws Exception {
                    try {
                        long waitTime = (long) (Math.random() * 15_000);
                        System.out.println("Waiting " + waitTime);
                        Thread.sleep(waitTime);
                        return "You have selected item: " + selectedCopy;
                    } catch (InterruptedException ex) {
                        System.out.println("Task interrupted!");
                        throw ex;
                    }
                }

            };
        }

        @Override
        protected void succeeded() {
            System.out.println("Service succeeded.");
        }

        @Override
        protected void cancelled() {
            System.out.println("Service cancelled.");
        }

    }
}

当您调用 Service.start() 时,它会创建一个 Task 并使用其 executor property 中包含的当前 Executor 执行它。如果 属性 包含 null 则它使用一些未指定的默认 Executor(使用守护线程)。

在上面,你看到我在需要时调用 reset() after cancelling and in the onSucceeded and onFailed callbacks. This is because a Service can only be started when in the READY state. You can use restart() 而不是 start()。基本上等同于调用cancel()->reset()->start().

1Task 无法恢复。相反,Service 每次启动时都会创建一个新的 Task


当您取消 Service 时,它会取消当前 运行ning Task,如果有的话。即使 ServiceTask 已被取消 并不意味着执行实际上已停止 。在Java中,取消后台任务需要与该任务的开发者合作。

这种合作采取定期检查执行是否应该停止的形式。如果使用正常的 RunnableCallable 这将需要检查当前 Thread 的中断状态或使用一些 boolean 标志 2 .由于 Task 扩展了 FutureTask you can also use the isCancelled() method inherited from the Future 接口。如果由于某种原因(调用外部代码,不使用 Task 等)无法使用 isCancelled(),则使用以下方法检查线程中断:

您可以通过 Thread.currentThread().

获取对当前线程的引用

在您的后台代码中,如果当前线程已被中断、boolean 标志已设置或 Task 已被取消,您需要在适当的位置检查。如果有,那么您将执行任何必要的清理并停止执行(通过 returning 或抛出异常)。

此外,如果您的 Thread 正在等待一些可中断的操作,例如阻塞 IO,那么它会在被中断时抛出一个 InterruptedException。在您的 MVCE 中,您使用 Thread.sleep 这是可中断的;这意味着当您调用取消时,此方法将抛出上述异常。

当我在上面说 "clean up" 时,我的意思是任何必要的清理工作 在后台 因为您仍在后台线程中。如果您需要清理 FX 线程上的任何内容(例如更新 UI),那么您可以使用 Service.

onCancelled 属性

在上面的代码中,您还会看到我使用了受保护的方法 succeeded()cancelled()TaskService 都提供了这些方法(以及其他用于各种 Worker.State 的方法)并且它们将始终在 FX 线程上调用。但是,请阅读文档,因为 ScheduledService 要求您为其中一些方法调用超级实现。

2如果使用 boolean 标志,请确保对它的更新对其他线程可见。您可以通过使它成为 volatile、同步它或使用 java.util.concurrent.atomic.AtomicBoolean.

来做到这一点

如果出于某种原因,您无法使用基于取消的解决方案使其在您的实际用例中工作(参见 , ,实际上是所有其他答案),您还需要做一件事可以试试

这里提出的解决方案理论上比任何基于取消的解决方案更差(因为所有冗余计算将继续直到完成,从而浪费资源)。但是,实际上,有效的解决方案会比无效的解决方案更好(前提是它对您有效,并且只有 有效 - 所以请检查 第一)。


我建议你完全放弃取消,而是使用一个简单的 volatile 字段来存储最后选择的项目的值:

private volatile String lastSelectedItem = null;

选择新标签后,您将更新此字段:

lastSelectedItem = task.selectedItem;

然后,在 on-succeeded 事件中,您只需检查是否允许分配计算结果:

if (task.selectedItem.equals(lastSelectedItem))

在下面找到整个修改后的 MCVE:

public class TaskRaceWithoutCancelation extends Application {

    private final ListView<String> listView = new ListView<>();
    private final Label label = new Label("Nothing selected");

    private final long startMillis = System.currentTimeMillis();

    private volatile String lastSelectedItem = null;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {

        // Simple UI
        VBox root = new VBox(5);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));
        root.getChildren().addAll(listView, label);

        // Populate the ListView
        listView.getItems().addAll(
                "One", "Two", "Three", "Four", "Five"
        );


        // Add listener to the ListView to start the task whenever an item is selected
        listView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {

            if (newValue != null) {
                // Create the background task
                MyTask task = new MyTask();
                lastSelectedItem = task.selectedItem;

                // Update the label when the task is completed
                task.setOnSucceeded(event -> {
                    if (task.selectedItem.equals(lastSelectedItem)) {
                        label.setText(task.getValue());
                        println("Assigned " + task.selectedItem);
                    }
                });

                new Thread(task).start();
            }

        });

        stage.setScene(new Scene(root));
        stage.show();

    }

    private void println(String string) {
        System.out.format("%5.2fs: %s%n", 0.001 * (System.currentTimeMillis() - startMillis), string);
    }

    private class MyTask extends Task<String> {

        final String selectedItem = listView.getSelectionModel().getSelectedItem();

        @Override
        protected String call() {
            int ms = new Random().nextInt(10000);
            println(String.format("Will return %s in %.2fs", selectedItem, 0.001 * ms));

            // Do long-running task (takes random time)
            long limitMillis = System.currentTimeMillis() + ms;
            while (System.currentTimeMillis() < limitMillis) {
            }

            println("Returned " + selectedItem);
            return "You have selected item: " + selectedItem;
        }
    }
}