So stoppen Sie ein zur wiederholten Ausführung geplantes Runnable nach einer bestimmten Anzahl von Ausführungen
Lesezeit: 14 Minuten
Spycho
Lage
Ich habe ein Runnable. Ich habe eine Klasse, die dieses Runnable für die Ausführung mit einem ScheduledExecutorService mit plant scheduleWithFixedDelay.
Tor
Ich möchte diese Klasse ändern, um das Runnable für die Ausführung mit fester Verzögerung zu planen entweder unbegrenzt, oder bis es eine bestimmte Anzahl von Malen ausgeführt wurde, abhängig von einigen Parametern, die an den Konstruktor übergeben werden.
Wenn möglich, möchte ich dasselbe Runnable verwenden, da es konzeptionell dasselbe ist, was “ausgeführt” werden sollte.
Mögliche Ansätze
Ansatz Nr. 1
Haben Sie zwei Runnables, eines, das den Zeitplan nach einer Reihe von Ausführungen abbricht (die gezählt werden) und eines, das dies nicht tut:
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
}
}
Ich hätte lieber nur ein Runnable für die Ausführung der doSomething-Methode. Die Planung an das Runnable zu binden, fühlt sich falsch an. Was denkst du darüber?
Ansatz Nr. 2
Haben Sie ein einzelnes Runnable für die Ausführung des Codes, den wir regelmäßig ausführen möchten. Haben Sie ein separates geplantes Runnable, das überprüft, wie oft das erste Runnable ausgeführt wurde, und abbricht, wenn es eine bestimmte Anzahl erreicht. Dies ist möglicherweise nicht genau, da es asynchron wäre. Es fühlt sich etwas umständlich an. Was denkst du darüber?
Ansatz Nr. 3
Erweitern Sie ScheduledExecutorService und fügen Sie eine Methode “scheduleWithFixedDelayNTimes” hinzu. Vielleicht existiert eine solche Klasse bereits? Derzeit verwende ich Executors.newSingleThreadScheduledExecutor(); um meine ScheduledExecutorService-Instanz zu erhalten. Ich müsste vermutlich eine ähnliche Funktionalität implementieren, um den erweiterten ScheduledExecutorService zu instanziieren. Das könnte schwierig sein. Was denkst du darüber?
Kein Scheduler-Ansatz [Edit]
Ich konnte keinen Planer verwenden. Ich könnte stattdessen so etwas haben wie:
for(int i = 0; i < numTimesToRun; i++){
doSomething();
Thread.sleep(delay);
}
Und führen Sie das in einem Thread aus. Was halten Sie davon? Sie könnten möglicherweise immer noch das Runnable verwenden und die Methode run direkt aufrufen.
Alle Vorschläge willkommen. Ich suche eine Debatte, um den “Best Practice”-Weg zu finden, um mein Ziel zu erreichen.
Brücken
Sie können die Methode cancel() für Future verwenden. Aus den Javadocs von Fahrplan zum Festpreis
Otherwise, the task will only terminate via cancellation or termination of the executor
Hier ist ein Beispielcode, der ein Runnable in ein anderes umschließt, das nachverfolgt, wie oft das Original ausgeführt wurde, und nach dem Ausführen von N-mal abbricht.
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);
}
}
Das ist ziemlich genau das, was @JB Nizet vorschlägt. Ich weiß, wie man ein geplantes Runnable storniert. Was ich wissen möchte, ist der am besten geeignete Weg, es in dieser Situation zu stornieren. Ihre Lösung bindet die Planung an das Runnable selbst, von dem ich nicht so überzeugt bin.
– Spycho
4. September 2011 um 14:41 Uhr
eine praktische Methode hinzugefügt, um eine Aufgabe n-mal auszuführen, um zu zeigen, dass die Planung nicht an das Runnable gebunden ist, das die Arbeit erledigt
– Brücken
4. September 2011 um 14:48 Uhr
Oh toll. Ich habe deine Aussage anfangs falsch verstanden. Ich verstehe was du meinst. Sie packen also das Runnable, das Sie ausführen möchten, in ein anderes Runnable, das für die N-malige Ausführung verantwortlich ist. Das klingt für mich nach einem guten Design. Es trennt das Scheduling vom eigentlichen Runnable und ermöglicht die Wiederverwendung der FixedExecutionRunnable-Runnable-Klasse. Was halten andere von diesem Ansatz?
– Spycho
4. September 2011 um 14:54 Uhr
Ich habe über diese Lösung nachgedacht … Was halten Sie davon, die runNTimes Methode? Dies würde das eigentliche Scheduling außerhalb des Runnable belassen. Das würde eine Planung zu festen Raten / mit festen Verzögerungen ermöglichen, ohne dass eine Reihe von Methoden hinzugefügt werden müssten. Alles, was das Runnable benötigen würde, wäre eine maximale Anzahl und das Delegate Runnable. Was haltet ihr davon?
– Spycho
5. September 2011 um 9:06 Uhr
Ah, das geht nicht, denn dann hätte das Runnable keinen Zugriff auf die Zukunft … Und Sie könnten die Zukunft nicht an den Konstruktor übergeben, weil sie zum Zeitpunkt der Instanziierung nicht geplant gewesen wäre.
Erstellt und führt eine periodische Aktion aus, die zuerst nach der angegebenen anfänglichen Verzögerung und anschließend mit der angegebenen Verzögerung zwischen dem Ende einer Ausführung und dem Beginn der nächsten aktiviert wird. Wenn bei einer Ausführung der Aufgabe eine Ausnahme auftritt, werden nachfolgende Ausführungen unterdrückt. Andernfalls wird die Aufgabe nur durch Stornierung oder Beendigung des Ausführenden beendet.
Also am einfachsten wäre es “Einfach eine Ausnahme auslösen” (obwohl dies als schlechte Praxis angesehen wird):
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);
}
Das würde bedeuten, eine “Ausnahme” unter nicht außergewöhnlichen Umständen auszulösen … Ich mag die Semantik davon nicht wirklich …
– Spycho
1. September 2011 um 11:38 Uhr
Es ist ähnlich wie das Auslösen einer InterruptedException, scheint mir die offensichtlichste und sauberste Lösung zu sein.
– Gehirn
1. September 2011 um 13:21 Uhr
Ich denke, es ist eine schreckliche Idee, eine Ausnahme auszulösen, um die Ausführung unter nicht außergewöhnlichen Umständen einfach abzubrechen, zumal es geeignete Möglichkeiten gibt (wie in der Antwort von sbridges), die Ausführung zu stoppen.
– Janick Bernet
11. September 2011 um 2:32 Uhr
Zusätzlich zu den obigen Kommentaren würde ich hinzufügen ++ ist nicht atomar, sodass Sie nicht die richtige Anzahl von Iterationen sicherstellen. Verwenden AtomicInteger.getAndIncrement stattdessen.
– Sieg
12. Mai 2015 um 23:15 Uhr
@Victory Sicher, im Allgemeinen könnte dies ein Problem sein. In diesem speziellen Fall verwende ich jedoch einen Ausführungsdienst mit einem einzelnen Thread, sodass dies keine Rolle spielen sollte
– Dawe
13. Mai 2015 um 11:36 Uhr
Janick Bernet
Bis jetzt Brücken Die Lösung scheint die sauberste zu sein, abgesehen von dem, was Sie erwähnt haben, dass sie die Verantwortung für die Handhabung der Anzahl der Ausführungen dem überlässt Runnable selbst. Es sollte sich nicht darum kümmern, stattdessen sollten die Wiederholungen ein Parameter der Klasse sein, die das Scheduling behandelt. Um dies zu erreichen, würde ich das folgende Design vorschlagen, das eine neue Executor-Klasse für einführt Runnables. Die Klasse stellt zwei öffentliche Methoden zum Planen von Aufgaben bereit, die Standard sind Runnables, mit endlicher oder unendlicher Wiederholung. Das Gleiche Runnable kann auf Wunsch für endliches und unendliches Scheduling übergeben werden (was nicht bei allen vorgeschlagenen Lösungen möglich ist, die das erweitern Runnable Klasse zur Bereitstellung endlicher Wiederholungen). Die Behandlung des Abbrechens endlicher Wiederholungen ist vollständig in der Scheduler-Klasse gekapselt:
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)
}
}
}
}
}
}
}
Um fair zu sein, die Logik der Verwaltung der Wiederholungen ist immer noch vorhanden aRunnableaber es ist ein Runnable völlig intern zu den MaxNSchedulerwährend die Runnable Eine zur Terminierung übergebene Aufgabe muss sich nicht um die Art der Terminierung kümmern. Auch dieses Anliegen könnte, falls gewünscht, leicht in den Planer verschoben werden, indem jedes Mal ein Rückruf bereitgestellt wird RunnableWrapper.run wurde ausgeführt. Dies würde den Code etwas verkomplizieren und die Notwendigkeit einführen, eine Karte zu führen RunnableWrappers und die entsprechenden Wiederholungen, weshalb ich mich dafür entschieden habe, die Zähler in der zu belassen RunnableWrapper Klasse.
Ich habe auch etwas Synchronisierung auf dem Wrapper hinzugefügt, als ich das Selbst gesetzt habe. Dies wird benötigt, da theoretisch, wenn die Ausführungen beendet sind, Selbst noch nicht zugewiesen wurde (ein ziemlich theoretisches Szenario, aber nur für 1 Wiederholung möglich).
Das Abbrechen wird elegant gehandhabt, ohne eine zu werfen InterruptedException und für den Fall, dass vor der Ausführung des Abbruchs eine weitere Runde angesetzt wird, die RunnableWrapper wird den Basiswert nicht aufrufen Runnable.
Der Wrapper ist ein netter Ansatz. Es scheint ziemlich sauber zu sein. Sie müssten zwei weitere Methoden hinzufügen, wenn Sie eine Terminierung mit festem Intervall statt mit fester Verzögerung zulassen wollten, aber das ist nicht das Ende der Welt.
– Spycho
11. September 2011 um 12:31 Uhr
@Spycho: Sicher, die Schnittstelle sollte trotzdem erweitert werden, um die Startverzögerung, das Intervall usw. angeben zu können. Ich werde das der Antwort hinzufügen.
– Janick Bernet
11. September 2011 um 12:39 Uhr
Jérôme Verstrynge
Hier ist mein Vorschlag (ich glaube, er behandelt alle in der Frage genannten Fälle):
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();
}
}
}
Darüber hinaus ermöglicht es einer externen Partei, alles aus dem Weg zu räumen Future zurückgegeben durch die submit() Methode.
Für Anwendungsfälle wie Abfragen bis zu einem bestimmten Timeout können wir mit einer einfacheren Lösung vorgehen 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
}
JB Niet
Ihr erster Ansatz scheint in Ordnung zu sein. Sie können beide Arten von Runnables kombinieren, indem Sie die übergeben mode -Objekt an seinen Konstruktor (oder übergeben Sie -1 als maximale Anzahl von Malen, die es ausführen muss), und verwenden Sie diesen Modus, um zu bestimmen, ob das Runnable abgebrochen werden muss oder nicht:
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
}
}
}
Sie müssen die geplante Zukunft an Ihre Aufgabe übergeben, damit sie sich selbst abbricht, oder Sie lösen möglicherweise eine Ausnahme aus.
ALZ
Ich habe genau die gleiche Funktionalität gesucht und gewählt org.springframework.scheduling.Trigger.
Unten ist ein vollständiges Test-Arbeitsbeispiel (sorry, wenn zu viel Code überschwemmt wird) applicationContext.xml