简体   繁体   中英

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

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.

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 reusable 1 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 Task s 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() .

1 The 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 flag 2 . 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.State s) 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.

2 If 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 .

Calling javafx.concurrent.Service.cancel() will cancel any currently running Task , making it throw an InterruptedException .

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

If additional cleanup is required one can provide Task.onCancelled() event handler.

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

    }
}

The 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");
    }
}

Explaination The demo application acts as a listener for System.in (keyboard). If any character pressed (with Enter), it will cancel any current thread if existing and submit a new one.

The Service Thread demo has a run() method which is calling doStuff() and displayStuff(). You can inject UI part for displayStuff. In the doStuff(), it is catching InterupptedException, if that exception happens, we don't perform displayStuff().

Please check and let see if it fits for your case. We can work out more.

The simplest code that does what you need is this:

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;
    }
}

Note that it's quite similar to Nghia Do's answer , only with synchronization and without all this extra demo code.

You commented that you're confused how the mentioned answer allows you to cancel all previous tasks, but the fact is that - when you synchronize properly (like in registerTask ) - you only need to store the last task because all previous tasks will already be cancelled (by the same registerTask method, which - due to being synchronized - will never execute concurrently). And as far as I know, calling cancel on a Task that has already completed is a no-op.

Moreover, using an ExecutorService over new Thread(task).start() (like Nghia Do proposed) is also a good idea, because Thread creation is expensive .


EDIT : I haven't given your MCVE a try before, sorry. Now that I have, and that I haven't been able to reproduce the behavior, I noticed the following flaws in your MCVE:

  1. You're using Thread.sleep() to simulate calculations, but calling Future.cancel() when inside Thread.sleep() terminates the task immediately because Future.cancel() calls Thread.interrupt() inside, and Thread.sleep() checks for interruption flag, which you most likely don't do in your real code.

  2. You're assigning the result of the calculation to a private field. Since (as mentioned above) the calculation was interrupted in your original MCVE immediately, and probably wasn't interrupted in your real code, in your real code the value of the field could get overwritten by an already canceled task.

  3. I believe that in your real code, you probably do more than just assining the result to the field. In the original MCVE, I wasn't able to reproduce the behavior even after replacing Thread.sleep() with a loop. The reason for this is that the onSucceeded handler does not get called, when the task was canceled (which is good). I believe, though, that in your real code, you may have done something more important (like some GUI update) in Task.call() than what you've shown in MCVE, because otherwise, as far as I understand, you wouldn't have experienced the original problem.

Here's a modification of your MCVE that should not exhibit the original problem.

EDIT : I further modified the MCVE to print cancelation info and also print times of events. This code, however, does not reproduce the behavior the OP wrote about.

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;
        }
    }
}

If, for whatever reason, you are unable to make it work in your real use case using cancelation-based solutions (cf. my answer , Slaw's answer , and actually all the other answers), there's one more thing you may try.

The solution proposed here is theoretically worse than any cancelation-based one (because all the redundant calculations will continue until done, thus wasting resources). However, practically, a working solution will be better than a non-working one (provided that it works for you, and that it's the only one that does - so please check Slaw's solution first).


I propose that you forgo cancelation completely, and instead use a simple volatile field to store the value of the last selected item:

private volatile String lastSelectedItem = null;

Upon selecting a new label, you would update this field:

lastSelectedItem = task.selectedItem;

Then, in the on-succeeded event, you would simply check whether you're allowed to assign the result of the computation:

if (task.selectedItem.equals(lastSelectedItem))

Find the entire modified MCVE below:

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;
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM