简体   繁体   English

如何在一定次数的执行后停止计划重复执行的Runnable

[英]How to stop a Runnable scheduled for repeated execution after a certain number of executions

Situation 情况

I have a Runnable. 我有一个Runnable。 I have a class that schedules this Runnable for execution using a ScheduledExecutorService with scheduleWithFixedDelay . 我有一个类,使用ScheduledExecutorService和scheduleWithFixedDelay来调度此Runnable以执行。

Goal 目标

I want to alter this class to schedule the Runnable for fixed delay execution either indefinitely, or until it has been run a certain number of times, depending on some parameter that is passed in to the constructor. 我想改变这个班安排了Runnable固定延迟执行无限期直至它已经运行一定的次数,取决于被传递给构造函数的参数一些。

If possible, I would like to use the same Runnable, as it is conceptually the same thing that should be "run". 如果可能的话,我想使用相同的Runnable,因为它在概念上应该是“运行”相同的东西。

Possible approaches 可能的方法

Approach #1 方法#1

Have two Runnables, one that cancels the schedule after a number of executions (which it keeps a count of) and one that doesn't: 有两个Runnables,一个在多次执行后(它保持计数)取消计划,另一个不执行:

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

I would rather just have one Runnable for the execution of the doSomething method. 我宁愿只有一个Runnable来执行doSomething方法。 Tying the scheduling to the Runnable feels wrong. 将调度绑定到Runnable感觉不对。 What do you think about this? 你怎么看待这件事?

Approach #2 方法#2

Have a single Runnable for the execution of the code that we want to run periodically. 有一个Runnable用于执行我们想要定期运行的代码。 Have a separate scheduled runnable that checks how many times the first Runnable has run and cancels when it gets to a certain amount. 有一个单独的调度runnable,用于检查第一个Runnable运行的次数,并在达到一定量时取消。 This may not be accurate, as it would be asynchronous. 这可能不准确,因为它是异步的。 It feels a bit cumbersome. 感觉有点麻烦。 What do you think about this? 你怎么看待这件事?

Approach #3 方法#3

Extend ScheduledExecutorService and add a method "scheduleWithFixedDelayNTimes". 扩展ScheduledExecutorService并添加方法“scheduleWithFixedDelayNTimes”。 Perhaps such a class already exists? 也许这样的课程已经存在? Currently, I'm using Executors.newSingleThreadScheduledExecutor(); 目前,我正在使用Executors.newSingleThreadScheduledExecutor(); to get my ScheduledExecutorService instance. 获取我的ScheduledExecutorService实例。 I would presumably have to implement similar functionality to instantiate the extended ScheduledExecutorService. 我可能必须实现类似的功能来实例化扩展的ScheduledExecutorService。 This could be tricky. 这可能很棘手。 What do you think about this? 你怎么看待这件事?

No scheduler approach [Edit] 没有调度程序方法[编辑]

I could not use a scheduler. 我无法使用调度程序。 I could instead have something like: 我可以改为:

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

And run that in some thread. 并在某个线程中运行它。 What do you think of that? 你对那个怎么想的? You could potentially still use the runnable and call the run method directly. 您可能仍然可以使用runnable并直接调用run方法。


Any suggestions welcome. 欢迎任何建议。 I'm looking for a debate to find the "best practice" way of achieving my goal. 我正在寻找辩论,找到实现目标的“最佳实践”方式。

You can use the cancel() method on Future. 您可以在Future上使用cancel()方法。 From the javadocs of scheduleAtFixedRate 来自scheduleAtFixedRate的javadoc

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

Here is some example code that wraps a Runnable in another that tracks the number of times the original was run, and cancels after running N times. 下面是一些示例代码,它将Runnable包装在另一个中,跟踪原始运行的次数,并在运行N次后取消。

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

Quoted from the API description ( ScheduledExecutorService.scheduleWithFixedDelay ): 引用API描述( ScheduledExecutorService.scheduleWithFixedDelay ):

Creates and executes a periodic action that becomes enabled first after the given initial delay, and subsequently with the given delay between the termination of one execution and the commencement of the next. 创建并执行一个周期性动作,该动作在给定的初始延迟之后首先被启用,并且随后在一次执行的终止和下一次执行的开始之间给定延迟。 If any execution of the task encounters an exception, subsequent executions are suppressed. 如果任务的任何执行遇到异常,则后续执行被禁止。 Otherwise, the task will only terminate via cancellation or termination of the executor. 否则,任务将仅通过取消或终止执行者来终止。

So, the easiest thing would be to "just throw an exception" (even though this is considered bad practice): 所以,最简单的事情是“只是抛出异常” (即使这被认为是不好的做法):

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

So far sbridges solution seems to be the cleanest one, except for what you mentioned, that it leaves the responsibility of handling the number of executions to the Runnable itself. 到目前为止, sbridges解决方案似乎是最干净的解决方案,除了你提到的,它还有责任处理Runnable本身的执行次数。 It should not be concerned with this, instead the repetitions should be a parameter of the class handling the scheduling. 它不应该与此有关,而是重复应该是处理调度的类的参数。 To achieve this, I would suggest the following design, that introduces a new executor class for Runnables . 为实现这一点,我建议使用以下设计,为Runnables引入一个新的执行器类。 The class provides two public methods for scheduling tasks, which are standard Runnables , with finite or infinite repetition. 该类提供了两种用于调度任务的公共方法,它们是标准的Runnables ,具有有限或无限重复。 The same Runnable can be passed for finite and infinite scheduling, if desired (which is not possible with all proposed solutions that extend the Runnable class to provide finite repetitions). 如果需要,可以传递相同的Runnable用于有限和无限调度(这对于扩展Runnable类以提供有限重复的所有提议的解决方案是不可能的)。 The handling of canceling finite repetitions is completely encapsulated in the scheduler class: 取消有限重复的处理完全封装在调度程序类中:

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

}

To be fair, the logic of managing the repetitions is still with a Runnable , but it'a a Runnable completely internal to the MaxNScheduler , whereas the Runnable task passed for scheduling has to not concern itself with the nature of the scheduling. 为了公平起见,管理重复的逻辑仍然是一个 Runnable ,但it'a一个Runnable完全内部的MaxNScheduler ,而Runnable通过调度任务必须不与调度的本质关注自身。 Also this concern could be easily moved out into the scheduler if desired, by providing some callback every time RunnableWrapper.run was executed. 如果需要,通过每次执行RunnableWrapper.run时提供一些回调,这个问题也可以很容易地移出到调度程序中。 This would complicate the code slightly and would introduce the need of keeping some map of RunnableWrapper s and the corresponding repetitions, which is why I opted for keeping the counters in the RunnableWrapper class. 这会使代码稍微复杂化,并且需要保留一些RunnableWrapper的映射以及相应的重复,这就是我选择将计数器保留在RunnableWrapper类中的原因。

I also added some synchronization on the wrapper when setting the self. 设置self时,我还在包装器上添加了一些同步。 This is needed as theoretically, when the executions finish, self might not have been assigned yet (a quite theoretical scenario, but for only 1 repetition possible). 理论上需要这样做,当执行结束时,可能还没有分配自我(一个相当理论的场景,但只能重复一次)。

The cancelling is handled gracefully, without throwing an InterruptedException and in case before the cancel is executed, another round is scheduled, the RunnableWrapper will not call the underlying Runnable . 正确处理取消,不会抛出InterruptedException ,如果在执行取消之前调度另一轮, RunnableWrapper将不会调用底层的Runnable

For use cases like polling until a certain timeout, we can approach with a simpler solution using Future.get() . 对于像轮询到某个超时之类的用例,我们可以使用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
}

Your first approach seems OK. 你的第一种方法似乎没问题 You could combine both types of runnables by passing the mode object to its constructor (or pass -1 as the max number of times it must run), and use this mode to determine if the runnable must be canceled or not : 您可以通过将mode对象传递给它的构造函数来组合两种类型的runnable(或者将-1作为必须运行的最大次数传递-1),并使用此模式来确定是否必须取消runnable:

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

You'll have to pass the scheduled future to your task in order for it to cancel itself, or you might throw an exception. 您必须将预定的未来传递给您的任务才能使其自行取消,否则您可能会抛出异常。

Here is my suggestion (I believe it handles all cases mentioned in the 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();
        }

    }

}

In addition, it allows an external party to stop everything from the Future returned by the submit() method. 此外,它允许外部方停止来自submit()方法返回的Future所有内容。

Usage: 用法:

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

I've been looking for exact same functionality and chose org.springframework.scheduling.Trigger . 我一直在寻找完全相同的功能,并选择了org.springframework.scheduling.Trigger

Below is full-test working example (sorry if too much flood in code) applicationContext.xml 下面是完整测试工作示例(如果代码中存在太多洪水,则抱歉)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 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!");


    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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