Stop a periodic task from within the task itself running in a ScheduledExecutorService

14.5k views Asked by At

Is there a nice way to stop the repetition of task from within the task itself when running in a ScheduledExecutorService?

Lets say, I have the following task:

Future<?> f = scheduledExecutor.scheduleAtFixedRate(new Runnable() {
    int count = 0;
    public void run() {
       System.out.println(count++);
       if (count == 10) {
           // ??? cancel self
       }
    }
}, 1, 1, TimeUnit.SECONDS);

From outside, it is easy to cancel via f.cancel(), but how can I stop the repetition at the specified place? (Passing the Future through an AtomicReference is not safe, because there is a potential window when the scheduleAtFixedRate returns f late and the variable is set late too, and the task itself might already run, seeing a null in the reference.)

5

There are 5 answers

4
Peter Lawrey On BEST ANSWER

When a repeating task throws an Exception or Error, it is placed in the Future and the task is not repeated again. You can throw a RuntimeException or Error of your choice.

0
Alb On

It seems like bad design for the Runnable to know anything about the executor it is running in, or to throw an error if reaching 10 is not an error state is a hack.

Can you do the loop to 10 outside of the scheduling and execution? This may require using a non-scheduling executor as you'd be scheduling them manually yourself.

1
Reboot On

Instead of using an anonymous inner class you can use a named class which can then have a property for the Future object you get from the Executor when you schedule a task.

abstract class FutureRunnable implements Runnable {

    private Future<?> future;

    /* Getter and Setter for future */

}

When you schedule a task you can then pass the Future to the Runnable.

FutureRunnable runnable = new FutureRunnable() {

    public void run() {
        if (/* abort condition */)
            getFuture().cancel(false);
    }

};
Future<?> future = executor.scheduleAtFixedRate(runnable, ...);
runnable.setFuture(future);

Maybe you will have to make sure, that the task is not executed before the Future has been set, because otherwise you will get a NullPointerException.

0
bestsss On

Here is another way, that's even Thread safe;

    final Future<?>[] f = {null};
    f[0]=  scheduledExecutor.scheduleAtFixedRate(new Runnable() {
        int count = 0;
        public void run() {

            System.out.println(count++);
            if (count == 10) {
                Future<?> future;
                while(null==(future = f[0])) Thread.yield();//prevent exceptionally bad thread scheduling 
                future.cancel(false);
                return;
                //cancel self
            }
        }
    }, 1, 1, TimeUnit.SECONDS);
0
Michael Wiles On

Just saw this now... because I wanted to do the same thing... here is my solution, I suspect this is threadsafe.

First create a container for the Future:

public static class Cancel {
    private ScheduledFuture<?> future;

    public synchronized void setFuture(ScheduledFuture<?> future) {
        this.future = future;
    }

    public synchronized void stop() {
        LOG.debug("cancelling {}", future);
        future.cancel(false);
    }
}

And then the future code:

    final Cancel controller = new Cancel();

    synchronized (controller) {
        ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(() -> {
            if (<CONTINUE RUNNING CONDITION) {

            } else {
                // STOP SCHEDULABLE FUTURE
                controller.stop();
            }
        }, startTime, timeBetweenVisbilityChecks);
        controller.setFuture(future);
    }
}

So notice how the stop will not be callable until the future has been created and the future has been set on the controller.

Bear in mind that the Runnable is the anomymous inner class and this will get run in a different thread altogether.