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

Zephyr :

My application contains a ListView that kicks off a background task every time an item is selected. The background task then updates information on the UI when it completes successfully.

However, when the user quickly clicks one item after another, all these tasks continue and the last task to complete "wins" and updates the UI, regardless of which item was selected last.

What I need is to somehow ensure this task only has one instance of it running at any given time, so cancel all prior tasks before starting the new one.

Here is an MCVE that demonstrates the issue:

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();

    }
}

When clicking on several items in random order, the outcome is unpredictable. What I need is for the label to be updated to show the results from the last Task that was executed.

Do I need to somehow schedule the tasks or add them to a service in order to cancel all prior tasks?

EDIT:

In my real world application, the user selects an item from the ListView and the background task reads a database ( a complex SELECT statement) to get all the information associated with that item. Those details are then displayed in the application.

The issue that is happening is when the user selects an item but changes their selection, the data returned displayed in the application could be for the first item selected, even though a completely different item is now chosen.

Any data returned from the first (ie: unwanted) selection can be discarded entirely.

Slaw :

Your requirements, as this answer mentions, seem like a perfect reason for using a Service. A Service allows you to run one Task at any given time in a reusable1 manner. When you cancel a Service, via Service.cancel(), it cancels the underlying Task. The Service also keeps track of its own Task for you so you don't need to keep them in a list somewhere.

Using your MVCE what you'd want to do is create a Service that wraps your Task. Every time the user selects a new item in the ListView you'd cancel the Service, update the necessary state, and then restart the Service. Then you'd use the Service.setOnSucceeded callback to set the result to the Label. This guarantees that only the last successful execution will be returned to you. Even if previously cancelled Tasks still return a result the Service will ignore them.

You also don't need to worry about external synchronization (at least in your MVCE). All the operations that deal with starting, cancelling, and observing the Service happen on the FX thread. The only bit of code (featured below) not executed on the FX thread will be inside Task.call() (well, and the immediately assigned fields when the class gets instantiated which I believe happens on the JavaFX-Launcher thread).

Here is a modified version of your MVCE using a 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.");
        }

    }
}

When you call Service.start() it creates a Task and executes it using the current Executor contained in its executor property. If the property contains null then it uses some unspecified, default Executor (using daemon threads).

Above, you see I call 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() rather than start() if needed. It is basically equivalent to calling cancel()->reset()->start().

1The Task doesn't become resuable. Rather, the Service creates a new Task each time it's started.


When you cancel a Service it cancels the currently running Task, if there is one. Even though the Service, and therefore Task, have been cancelled does not mean that execution has actually stopped. In Java, cancelling background tasks requires cooperation with the developer of said task.

This cooperation takes the form of periodically checking if execution should stop. If using a normal Runnable or Callable this would require checking the interrupted status of the current Thread or using some boolean flag2. Since Task extends FutureTask you can also use the isCancelled() method inherited from the Future interface. If you can't use isCancelled() for some reason (called outside code, not using a Task, etc...) then you check for thread interruption using:

  • Thread.interrupted()
    • Static method
    • Can only check current Thread
    • Clears the interruption status of current thread
  • Thread.isInterrupted()
    • Instance method
    • Can check any Thread you have a reference to
    • Does not clear interruption status

You can get a reference to the current thread via Thread.currentThread().

Inside your background code you'd want to check at appropriate points if the current thread has been interrupted, the boolean flag has been set, or the Task has been cancelled. If it has then you'd perform any necessary clean up and stop execution (either by returning or throwing an exception).

Also, if your Thread is waiting on some interruptible operation, such as blocking IO, then it will throw an InterruptedException when interrupted. In your MVCE you use Thread.sleep which is interruptible; meaning when you call cancel this method will throw the mentioned exception.

When I say "clean up" above I mean any clean up necessary in the background since you're still on the background thread. If you need to clean up anything on the FX thread (such as updating the UI) then you can use the onCancelled property of Service.

In the code above you'll also see I use the protected methods succeeded() and cancelled(). Both Task and Service provide these methods (as well as others for the various Worker.States) and they will always be called on the FX thread. Read the documentation, however, as ScheduledService requires you to call the super implementations for some of these methods.

2If using a boolean flag make sure updates to it are visible to other threads. You can do this by making it volatile, synchronizing it, or using a java.util.concurrent.atomic.AtomicBoolean.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=69437&siteId=1