web-dev-qa-db-fra.com

Comment arrêter un Runnable prévu pour une exécution répétée après un certain nombre d'exécutions

Situation

J'ai un Runnable. J'ai une classe qui planifie l'exécution de ce Runnable en utilisant un ScheduledExecutorService avec scheduleWithFixedDelay .

Objectif

Je veux modifier cette classe pour planifier l'exécution de Runnable pour un délai fixe soit indéfiniment, ou jusqu'à ce qu'il ait été exécuté un certain nombre de fois, en fonction de certains paramètres transmis au constructeur.

Si possible, je voudrais utiliser le même Runnable, car c'est conceptuellement la même chose qui devrait être "exécutée".

Approches possibles

Approche n ° 1

Avoir deux Runnables, un qui annule la planification après un certain nombre d'exécutions (dont il compte) et l'autre qui ne le fait pas:

public class MyClass{
    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    public enum Mode{
        INDEFINITE, FIXED_NO_OF_TIMES
    }

    public MyClass(Mode mode){
        if(mode == Mode.INDEFINITE){
            scheduler.scheduleWithFixedDelay(new DoSomethingTask(), 0, 100, TimeUnit.MILLISECONDS);
        }else if(mode == Mode.FIXED_NO_OF_TIMES){
            scheduler.scheduleWithFixedDelay(new DoSomethingNTimesTask(), 0, 100, TimeUnit.MILLISECONDS);
        }
    }

    private class DoSomethingTask implements Runnable{
        @Override
        public void run(){
            doSomething();
        }
    }

    private class DoSomethingNTimesTask implements Runnable{
        private int count = 0;

        @Override
        public void run(){
            doSomething();
            count++;
            if(count > 42){
                // Cancel the scheduling.
                // Can you do this inside the run method, presumably using
                // the Future returned by the schedule method? Is it a good idea?
            }
        }
    }

    private void doSomething(){
        // do something
    }
}

Je préfère juste avoir un Runnable pour l'exécution de la méthode doSomething. Lier la planification à Runnable semble incorrect. Que pensez-vous de ceci?

Approche n ° 2

Avoir un Runnable unique pour l'exécution du code que nous voulons exécuter périodiquement. Avoir une exécutable planifiée distincte qui vérifie combien de fois la première exécutable a été exécutée et annule lorsqu'elle atteint un certain montant. Cela peut ne pas être précis, car il serait asynchrone. Cela semble un peu lourd. Que pensez-vous de ceci?

Approche n ° 3

Étendez ScheduledExecutorService et ajoutez une méthode "scheduleWithFixedDelayNTimes". Peut-être qu'une telle classe existe déjà? Actuellement, j'utilise Executors.newSingleThreadScheduledExecutor(); pour obtenir mon instance ScheduledExecutorService. Je devrais probablement implémenter des fonctionnalités similaires pour instancier le ScheduledExecutorService étendu. Cela pourrait être délicat. Que pensez-vous de ceci?

Aucune approche de planificateur [Modifier]

Je ne pouvais pas utiliser de planificateur. Je pourrais plutôt avoir quelque chose comme:

for(int i = 0; i < numTimesToRun; i++){
    doSomething();
    Thread.sleep(delay);
}

Et exécutez cela dans un fil. Que penses-tu de cela? Vous pouvez toujours utiliser runnable et appeler directement la méthode run.


Toutes les suggestions sont les bienvenues. Je recherche un débat pour trouver la "meilleure pratique" pour atteindre mon objectif.

56
Spycho

Vous pouvez utiliser la méthode cancel () sur Future. À partir des javadocs de scheduleAtFixedRate

Otherwise, the task will only terminate via cancellation or termination of the executor

Voici un exemple de code qui encapsule un Runnable dans un autre qui suit le nombre de fois que l'original a été exécuté et annule après avoir exécuté N fois.

public void runNTimes(Runnable task, int maxRunCount, long period, TimeUnit unit, ScheduledExecutorService executor) {
    new FixedExecutionRunnable(task, maxRunCount).runNTimes(executor, period, unit);
}

class FixedExecutionRunnable implements Runnable {
    private final AtomicInteger runCount = new AtomicInteger();
    private final Runnable delegate;
    private volatile ScheduledFuture<?> self;
    private final int maxRunCount;

    public FixedExecutionRunnable(Runnable delegate, int maxRunCount) {
        this.delegate = delegate;
        this.maxRunCount = maxRunCount;
    }

    @Override
    public void run() {
        delegate.run();
        if(runCount.incrementAndGet() == maxRunCount) {
            boolean interrupted = false;
            try {
                while(self == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
                self.cancel(false);
            } finally {
                if(interrupted) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    public void runNTimes(ScheduledExecutorService executor, long period, TimeUnit unit) {
        self = executor.scheduleAtFixedRate(this, 0, period, unit);
    }
}
61
sbridges

Extrait de la description de l'API ( ScheduledExecutorService.scheduleWithFixedDelay ):

Crée et exécute une action périodique qui devient activée d'abord après le délai initial donné, puis avec le délai donné entre la fin d'une exécution et le début de la suivante. Si une exécution de la tâche rencontre une exception, les exécutions suivantes sont supprimées. Sinon, la tâche ne se terminera que par l'annulation ou la résiliation de l'exécuteur testamentaire.

Donc, la chose la plus simple serait de "simplement lever une exception" (même si cela est considéré comme une mauvaise pratique):

static class MyTask implements Runnable {

    private int runs = 0;

    @Override
    public void run() {
        System.out.println(runs);
        if (++runs >= 20)
            throw new RuntimeException();
    }
}

public static void main(String[] args) {
    ScheduledExecutorService s = Executors.newSingleThreadScheduledExecutor();
    s.scheduleWithFixedDelay(new MyTask(), 0, 100, TimeUnit.MILLISECONDS);
}
8
dacwe

Jusqu'ici sbridges la solution semble être la plus propre, à l'exception de ce que vous avez mentionné, qu'elle laisse la responsabilité de gérer le nombre d'exécutions au Runnable lui-même. Il ne devrait pas s'en préoccuper, mais les répétitions devraient être un paramètre de la classe qui gère l'ordonnancement. Pour ce faire, je suggère la conception suivante, qui introduit une nouvelle classe d'exécuteur pour Runnables. La classe fournit deux méthodes publiques pour la planification des tâches, qui sont des Runnables standard, avec une répétition finie ou infinie. Le même Runnable peut être passé pour une planification finie et infinie, si vous le souhaitez (ce qui n'est pas possible avec toutes les solutions proposées qui étendent la classe Runnable pour fournir des répétitions finies). La gestion de l'annulation des répétitions finies est complètement encapsulée dans la classe du planificateur:

class MaxNScheduler
{

  public enum ScheduleType 
  {
     FixedRate, FixedDelay
  }

  private ScheduledExecutorService executorService =
     Executors.newSingleThreadScheduledExecutor();

  public ScheduledFuture<?> scheduleInfinitely(Runnable task, ScheduleType type, 
    long initialDelay, long period, TimeUnit unit)
  {
    return scheduleNTimes(task, -1, type, initialDelay, period, unit);
  }

  /** schedule with count repetitions */
  public ScheduledFuture<?> scheduleNTimes(Runnable task, int repetitions, 
    ScheduleType type, long initialDelay, long period, TimeUnit unit) 
  {
    RunnableWrapper wrapper = new RunnableWrapper(task, repetitions);
    ScheduledFuture<?> future;
    if(type == ScheduleType.FixedDelay)
      future = executorService.scheduleWithFixedDelay(wrapper, 
         initialDelay, period, TimeUnit.MILLISECONDS);
    else
      future = executorService.scheduleAtFixedRate(wrapper, 
         initialDelay, period, TimeUnit.MILLISECONDS);
    synchronized(wrapper)
    {
       wrapper.self = future;
       wrapper.notify(); // notify wrapper that it nows about it's future (pun intended)
    }
    return future;
  }

  private static class RunnableWrapper implements Runnable 
  {
    private final Runnable realRunnable;
    private int repetitions = -1;
    ScheduledFuture<?> self = null;

    RunnableWrapper(Runnable realRunnable, int repetitions) 
    {
      this.realRunnable = realRunnable;
      this.repetitions = repetitions;
    }

    private boolean isInfinite() { return repetitions < 0; }
    private boolean isFinished() { return repetitions == 0; }

    @Override
    public void run()
    {
      if(!isFinished()) // guard for calls to run when it should be cancelled already
      {
        realRunnable.run();

        if(!isInfinite())
        {
          repetitions--;
          if(isFinished())
          {
            synchronized(this) // need to wait until self is actually set
            {
              if(self == null)
              {
                 try { wait(); } catch(Exception e) { /* should not happen... */ }
              }
              self.cancel(false); // cancel gracefully (not throwing InterruptedException)
            }
          }
        }
      }
    }
  }

}

Pour être honnête, la logique de gestion des répétitions est toujours avec aRunnable, mais c'est un Runnable complètement interne au MaxNScheduler, tandis que la tâche Runnable passée pour la planification ne doit pas se préoccuper de la nature de la planification. Cette préoccupation peut également être facilement déplacée dans le planificateur si vous le souhaitez, en fournissant un rappel à chaque fois RunnableWrapper.run a été exécuté. Cela compliquerait légèrement le code et introduirait la nécessité de conserver une carte de RunnableWrappers et les répétitions correspondantes, c'est pourquoi j'ai opté pour conserver les compteurs dans la classe RunnableWrapper.

J'ai également ajouté une synchronisation sur le wrapper lors de la configuration de l'auto. Ceci est nécessaire car théoriquement, lorsque les exécutions se terminent, self n'a peut-être pas encore été attribué (un scénario tout à fait théorique, mais pour une seule répétition possible).

L'annulation est gérée avec élégance, sans lancer de InterruptedException et au cas où avant que l'annulation soit exécutée, un autre tour est planifié, le RunnableWrapper n'appellera pas le Runnable sous-jacent.

5
Janick Bernet

Pour les cas d'utilisation comme l'interrogation jusqu'à un certain délai, nous pouvons approcher avec une solution plus simple en utilisant Future.get().

/* Define task */
public class Poll implements Runnable {
    @Override
    public void run() {
        // Polling logic
    }
}

/* Create executor service */
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

/* Schedule task - poll every 500ms */
ScheduledFuture<?> future = executorService.scheduleAtFixedRate(new Poll(), 0, 500, TimeUnit.MILLISECONDS);

/* Wait till 60 sec timeout */
try {
    future.get(60, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    scheduledFuture.cancel(false);
    // Take action on timeout
}
1
MukeshD

Voici ma suggestion (je crois qu'elle traite tous les cas mentionnés dans la question):

public class RepeatedScheduled implements Runnable {

    private int repeatCounter = -1;
    private boolean infinite;

    private ScheduledExecutorService ses;
    private long initialDelay;
    private long delay;
    private TimeUnit unit;

    private final Runnable command;
    private Future<?> control;

    public RepeatedScheduled(ScheduledExecutorService ses, Runnable command,
        long initialDelay, long delay, TimeUnit unit) {

        this.ses = ses;
        this.initialDelay = initialDelay;
        this.delay = delay;
        this.unit = unit;

        this.command = command;
        this.infinite = true;

    }

    public RepeatedScheduled(ScheduledExecutorService ses, Runnable command,
        long initialDelay, long delay, TimeUnit unit, int maxExecutions) {

        this(ses, command, initialDelay, delay, unit);
        this.repeatCounter = maxExecutions;
        this.infinite = false;

    }

    public Future<?> submit() {

        // We submit this, not the received command
        this.control = this.ses.scheduleWithFixedDelay(this,
            this.initialDelay, this.delay, this.unit);

        return this.control;

    }

    @Override
    public synchronized void run() {

        if ( !this.infinite ) {
            if ( this.repeatCounter > 0 ) {
                this.command.run();
                this.repeatCounter--;
            } else {
                this.control.cancel(false);
            }
        } else {
            this.command.run();
        }

    }

}

De plus, il permet à un tiers externe de tout arrêter à partir de la Future renvoyée par la méthode submit().

Usage:

Runnable MyRunnable = ...;
// Repeat 20 times
RepeatedScheduled rs = new RepeatedScheduled(
    MySes, MyRunnable, 33, 44, TimeUnit.SECONDS, 20);
Future<?> MyControl = rs.submit();
...
1

Votre première approche semble OK. Vous pouvez combiner les deux types de runnables en passant l'objet mode à son constructeur (ou passer -1 comme le nombre maximal d'exécutions) et utiliser ce mode pour déterminer si la runnable doit être annulée ou non :

private class DoSomethingNTimesTask implements Runnable{
    private int count = 0;
    private final int limit;

    /**
     * Constructor for no limit
     */
    private DoSomethingNTimesTask() {
        this(-1);
    }

    /**
     * Constructor allowing to set a limit
     * @param limit the limit (negative number for no limit)
     */
    private DoSomethingNTimesTask(int limit) {
        this.limit = limit;
    }

    @Override
    public void run(){
        doSomething();
        count++;
        if(limit >= 0 && count > limit){
            // Cancel the scheduling
        }
    }
}

Vous devrez transmettre le futur planifié à votre tâche pour qu'elle s'annule elle-même, ou vous pourriez lever une exception.

1
JB Nizet

Je cherchais exactement la même fonctionnalité et j'ai choisi org.springframework.scheduling.Trigger.

Ci-dessous est un exemple de test complet (désolé si trop d'inondation dans le code) applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:task="http://www.springframework.org/schema/task"
 xmlns:util="http://www.springframework.org/schema/util"
 xmlns:context="http://www.springframework.org/schema/context"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context/ http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/util/ http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">

    <bean id="blockingTasksScheduler" class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
        <property name="poolSize" value="10" />
    </bean>

    <task:scheduler id="deftaskScheduler" pool-size="10" />

</beans>

Java

package com.alz.springTests.schedulerTest;

import Java.time.LocalDateTime;
import Java.time.ZoneId;
import Java.time.temporal.ChronoUnit;
import Java.util.Date;
import Java.util.concurrent.ScheduledThreadPoolExecutor;
import Java.util.concurrent.ThreadLocalRandom;
import Java.util.concurrent.atomic.AtomicInteger;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

public class ScheduledTest {

    private static ApplicationContext applicationContext;
    private static TaskScheduler taskScheduler;

    private static final class SelfCancelableTask implements Runnable, Trigger {
        Date creationTime = new Date();
        AtomicInteger counter = new AtomicInteger(0);
        private volatile boolean shouldStop = false;
        private int repeatInterval = 3; //seconds

        @Override
        public void run() {
            log("task: run started");

            // simulate "doing job" started
            int sleepTimeMs = ThreadLocalRandom.current().nextInt(500, 2000+1);
            log("will sleep " + sleepTimeMs + " ms");
            try {
                Thread.sleep(sleepTimeMs);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // "doing job" finished

            int i = counter.incrementAndGet();
            if (i > 5) { //cancel myself
                logErr("Attempts exceeded, will mark as shouldStop");
                shouldStop = true;

            } else {
                log("task: executing cycle #"+i);
            }
        }

        @Override
        public Date nextExecutionTime(TriggerContext triggerContext) {
            log("nextExecutionTime: triggerContext.lastActualExecutionTime() " + triggerContext.lastActualExecutionTime());
            log("nextExecutionTime: triggerContext.lastCompletionTime() " + triggerContext.lastCompletionTime());
            log("nextExecutionTime: triggerContext.lastScheduledExecutionTime() " + triggerContext.lastScheduledExecutionTime());

            if (shouldStop) 
                return null;

            if (triggerContext.lastCompletionTime() == null) {
                LocalDateTime ldt = creationTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().plus(repeatInterval, ChronoUnit.SECONDS);
                return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
            } else {
                LocalDateTime ldt = triggerContext.lastCompletionTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().plus(repeatInterval, ChronoUnit.SECONDS);
                return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());               
            }

        }

    }

    private static void log(String log) {
        System.out.printf("%s [%s] %s\r\n", LocalDateTime.now(), Thread.currentThread(), log);
    }

    private static void logErr(String log) {
        System.err.printf("%s [%s] %s\r\n", LocalDateTime.now(), Thread.currentThread(), log);
    }

    public static void main(String[] args) {

        log("main: Stated...");

        applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");

        taskScheduler = (TaskScheduler) applicationContext.getBean("blockingTasksScheduler");

        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = ((ThreadPoolTaskScheduler)taskScheduler).getScheduledThreadPoolExecutor();

        SelfCancelableTask selfCancelableTask = new SelfCancelableTask();
        taskScheduler.schedule(selfCancelableTask, selfCancelableTask);


        int waitAttempts = 0;
        while (waitAttempts < 30) {
            log("scheduledPool pending tasks: " + scheduledThreadPoolExecutor.getQueue().size());

            try {
                Thread.sleep(1*1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            waitAttempts++;

        }

        log("main: Done!");


    }

}
0
ALZ