68 votes

Comment arrêter un Runnable programmé 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 programme l'exécution de ce Runnable à l'aide d'un ScheduledExecutorService avec scheduleWithFixedDelay.

Goal

Je veux modifier cette classe pour programmer l'exécution du Runnable pour un délai fixe soit indéfiniment, soit 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 #1

Avoir deux Runnables, l'un qui annule la programmation après un certain nombre d'exécutions (qu'il compte) et un 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){
                // Annuler la programmation.
                // Pouvez-vous le faire à l'intérieur de la méthode run, en utilisant
                // vraisemblablement le Future renvoyé par la méthode schedule? Est-ce une bonne idée?
            }
        }
    }

    private void doSomething(){
        // faîtes quelque chose
    }
}

Je préférerais avoir un seul Runnable pour l'exécution de la méthode doSomething. Lier la programmation au Runnable semble incorrect. Que pensez-vous de cela?

Approche #2

Avoir un seul Runnable pour l'exécution du code que nous voulons exécuter périodiquement. Avoir un Runnable programmé séparé qui vérifie combien de fois le premier Runnable a été exécuté et l'annule lorsqu'il atteint un certain montant. Cela peut ne pas être précis, car ce serait asynchrone. Cela semble un peu lourd. Que pensez-vous de cela?

Approche #3

Étendre ScheduledExecutorService et ajouter une méthode "scheduleWithFixedDelayNTimes". Peut-être qu'une telle classe existe déjà? Actuellement, j'utilise Executors.newSingleThreadScheduledExecutor(); pour obtenir l'instance de mon ScheduledExecutorService. Je devrais vraisemblablement implémenter une fonctionnalité similaire pour instancier le ScheduledExecutorService étendu. Cela pourrait être difficile. Que pensez-vous de cela?

Aucune approche de planificateur [Edit]

Je pourrais ne pas utiliser de planificateur. Je pourrais avoir quelque chose comme :

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

Et exécuter cela dans un thread. Que pensez-vous de cela? Vous pourriez potentiellement toujours utiliser le runnable et appeler la méthode run directement.


Toute suggestion est la bienvenue. Je recherche un débat pour trouver la façon "meilleure pratique" d'atteindre mon objectif.

72voto

sbridges Points 16284

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

Sinon, la tâche se terminera uniquement par annulation ou arrêt de l'exécuteur

Voici un exemple de code qui encapsule un Runnable dans un autre qui suit le nombre de fois où l'original a été exécuté, et annule après avoir été 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);
    }
}

4 votes

C'est à peu près ce que @JB Nizet suggère. Je sais comment annuler un Runnable programmé. Ce que je veux savoir, c'est la manière la plus appropriée de l'annuler dans cette situation. Votre solution relie la planification au Runnable lui-même, ce qui ne me convainc pas totalement.

0 votes

A ajouté une méthode pratique pour exécuter une tâche n fois pour montrer que la planification n'est pas liée au Runnable qui effectue le travail

3 votes

Oh génial. J'ai mal compris ce que vous disiez initialement. Je comprends ce que vous voulez dire. Vous enveloppez donc le Runnable que vous voulez exécuter dans un autre Runnable qui est responsable de l'exécution N fois. Cela me semble être une bonne conception. Cela sépare la planification du Runnable réel et permet la réutilisation de la classe de Runnable FixedExecutionRunnable. Que pensent les autres de cette approche ?

10voto

dacwe Points 26160

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

Crée et exécute une action périodique qui devient activée pour la première fois après le délai initial donné, et ensuite avec le délai donné entre la fin d'une exécution et le commencement 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 annulation ou terminaison de l'exécuteur.

Ainsi, la chose la plus simple serait de "juste lancer 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);
}

10 votes

Cela signifierait jeter une "exception" dans des circonstances non exceptionnelles... Je n'aime pas vraiment la sémantique de cela...

3 votes

C'est similaire à lever une InterruptedException, cela me semble être la solution la plus évidente et propre.

3 votes

Je pense que c'est une terrible idée de jeter une exception pour simplement annuler l'exécution dans des circonstances non exceptionnelles, surtout qu'il existe des moyens appropriés (comme dans la réponse de sbridges) pour arrêter l'exécution.

6voto

Janick Bernet Points 6465

Jusqu'à présent, la solution de sbridges semble être la plus propre, sauf pour ce que vous avez mentionné, à savoir qu'elle laisse la responsabilité de gérer le nombre d'exécutions au Runnable lui-même. Il ne devrait pas se soucier de cela, au lieu de cela, les répétitions devraient être un paramètre de la classe qui gère la planification. Pour y parvenir, je suggérerais la conception suivante, qui introduit une nouvelle classe exécuteur pour les Runnables. La classe fournit deux méthodes publiques pour planifier des tâches, qui sont des Runnables standard, avec répétition finie ou infinie. Le même Runnable peut être passé pour une planification finie et infinie, si désiré (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 de planification :

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

  /** planifier avec count répétitions */
  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(); // notifier le wrapper qu'il connaît maintenant son avenir (jeu de mots intentionnel)
    }
    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()) // garde pour les appels à run lorsqu'ils devraient être annulés
      {
        realRunnable.run();

        if(!isInfinite())
        {
          repetitions--;
          if(isFinished())
          {
            synchronized(this) // besoin d'attendre que self soit effectivement défini
            {
              if(self == null)
              {
                 try { wait(); } catch(Exception e) { /* cela ne devrait pas arriver... */ }
              }
              self.cancel(false); // annuler de manière élégante (sans lancer InterruptedException)
            }
          }
        }
      }
    }
  }

}

Pour être honnête, la logique de gestion des répétitions est toujours avec un Runnable, mais c'est un Runnable complètement interne au MaxNScheduler, alors 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 pourrait également être facilement déplacée dans le planificateur si désiré, en fournissant un certain retour d'appel à chaque fois que RunnableWrapper.run est exécuté. Cela compliquerait légèrement le code et introduirait le besoin de conserver une sorte de carte des RunnableWrapper et des répétitions correspondantes, c'est pourquoi j'ai choisi de conserver les compteurs dans la classe RunnableWrapper.

J'ai également ajouté une certaine synchronisation sur le wrapper lors du réglage du self. Cela est nécessaire car théoriquement, lorsque les exécutions se terminent, self pourrait ne pas avoir été attribué encore (un scénario assez théorique, mais possible pour une seule répétition).

L'annulation est gérée de manière élégante, sans lancer d'InterruptedException et dans le cas où avant que l'annulation ne soit exécutée, une autre série est planifiée, le RunnableWrapper ne appellera pas le Runnable sous-jacent.

0 votes

Le wrapper est une bonne approche. Il semble assez propre. Vous devriez ajouter deux autres méthodes si vous vouliez permettre la planification avec un intervalle fixe plutôt qu'un délai fixe, mais ce n'est pas la fin du monde.

0 votes

@Spycho: Bien sûr, l'interface devrait être étendue de toute façon pour permettre de spécifier le délai de démarrage, l'intervalle, etc. Je vais ajouter cela à la réponse.

2voto

JVerstry Points 12414

Voici ma suggestion (je crois qu'elle gère 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() {

        // Nous soumettons ceci, pas la commande reçue
        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, cela permet à une partie externe d'arrêter tout depuis le Future retourné par la méthode submit().

Utilisation:

Runnable MyRunnable = ...;
// Répéter 20 fois
RepeatedScheduled rs = new RepeatedScheduled(
    MySes, MyRunnable, 33, 44, TimeUnit.SECONDS, 20);
Future MyControl = rs.submit();
...

0 votes

Il semble que ce soit à peu près la même chose que la solution de @sbridges. Il a un mécanisme pour accéder au Futur, mais cela serait une trivialité dans l'autre solution. Y a-t-il des différences notables que j'aurais pu manquer?

0 votes

@Spycho Il gère le cas où il n'y a pas de limite au nombre d'exécutions et couvre le cas où le runnable fourni n'est pas sûr pour les threads.

1voto

JB Nizet Points 250258

Votre première approche semble OK. Vous pourriez combiner les deux types de runnables en passant l'objet mode à son constructeur (ou en passant -1 comme nombre maximum de fois qu'il doit s'exécuter), et utiliser ce mode pour déterminer si le runnable doit être annulé ou non :

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

    /**
     * Constructeur pour aucune limite
     */
    private DoSomethingNTimesTask() {
        this(-1);
    }

    /**
     * Constructeur permettant de définir une limite
     * @param limit la limite (nombre négatif pour aucune limite)
     */
    private DoSomethingNTimesTask(int limit) {
        this.limit = limit;
    }

    @Override
    public void run(){
        doSomething();
        count++;
        if(limit >= 0 && count > limit){
            // Annuler la planification
        }
    }
}

Vous devrez passer le futur planifié à votre tâche afin qu'elle puisse s'annuler elle-même, ou vous pourriez lever une exception.

0 votes

Alors, vous pensez que c'est OK de lier la planification au Runnable lui-même? Pour moi, il semble que le Runnable ne devrait pas être conscient de la planification. Pourriez-vous développer sur cela?

0 votes

+1 pour l'idée de combinaison. C'est beaucoup mieux que mon approche initiale.

0 votes

Lancer une exception est probablement plus facile et plus propre. Vous pouvez définir un type spécifique d'exception pour cela : LimitReachedException ou quelque chose comme ça.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X