简体   繁体   English

具有任务之间相对延迟的 ScheduledExecutorService

[英]ScheduledExecutorService with relative delay between tasks

I'm trying to make a ScheduledExecutorService where only one task is active at a time and only once a task has finished, the next task will begin its delay with an arbitrary delay amount.我正在尝试创建一个 ScheduledExecutorService,其中一次只有一个任务处于活动状态,并且只有在任务完成后,下一个任务才会以任意延迟量开始延迟。

As a very simple example of what I mean, take a look at this method.作为我的意思的一个非常简单的例子,看看这个方法。 The idea is to schedule 10 Runnables to simulate a countdown from 10-1.这个想法是安排 10 个 Runnables 来模拟从 10-1 开始的倒计时。 Each interval takes one second ( imagine this was an arbitrary amount of seconds though , I can't use scheduleAtFixedRate in my use case).每个间隔需要一秒钟(虽然想象这是任意秒数,但我不能在我的用例中使用 scheduleAtFixedRate)。

private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

public void startCountdown() {
  for (int i = 10; i > 0; i--) {
    int countdownNumber = i;
    scheduler.schedule(() -> {
      System.out.println(countdownNumber);
    }, 1, TimeUnit.SECONDS);
  }
}

However, this will simply print all 10 numbers at once, instead of waiting for a second between each value.但是,这只会一次打印所有 10 个数字,而不是在每个值之间等待一秒钟。 The only way I can circumvent this (to my knowledge) is calculating the ABSOLUTE delay, as opposed to the relative one.我能避免这种情况(据我所知)的唯一方法是计算绝对延迟,而不是相对延迟。

While it's possible to calculate the absolute time for each item, it would be quite a hassle.虽然可以计算每个项目的绝对时间,但这会很麻烦。 Isn't there some construct in Java that allows me to queue many items at once, but waits in between each item for the delay to finish, rather than processing every delay at once? Java 中是否有某种构造允许我一次对多个项目进行排队,但在每个项目之间等待延迟完成,而不是一次处理每个延迟?

tl;dr tl;博士

  • Do not use your countdown number to directly schedule your tasks.不要使用您的倒计时数字直接安排您的任务。 Have one number for scheduling number of seconds to wait (1,2,3,…) and another number for the countdown (9,8,7,…).有一个数字用于安排等待的秒数 (1,2,3,...) 和另一个数字用于倒计时 (9,8,7,...)。
  • Use scheduleAtFixedRate to schedule your tasks for an increasing number of seconds.使用scheduleAtFixedRate将您的任务安排在越来越多的秒数内。 No need for executor service to be single-threaded.执行程序服务不需要是单线程的。

Details细节

Task that re-schedules itself重新安排自己的任务

Isn't there some construct in Java that allows me to queue many items at once, but waits in between each item for the delay to finish, rather than processing every delay at once? Java 中是否有某种构造允许我一次对多个项目进行排队,但在每个项目之间等待延迟完成,而不是一次处理每个延迟?

If you have an arbitrary amount of time not known up front when beginning the scheduling, then you should only run one task at a time.如果在开始安排时您有任意数量的时间事先不知道,那么您应该一次只运行一个任务。 Let the task re-schedule itself.让任务重新安排自己。

To enable a task to re-schedule itself, pass a reference to the ScheduledExecutorService to the task object (your Runnable or Callable ) as an argument in the constructor.要使任务能够重新安排自身,请将对ScheduledExecutorService的引用作为构造函数中的参数传递给任务 object(您的RunnableCallable )。 After the task completes its main work, it discovers/calculates the amount of time to elapse for the next run.任务完成其主要工作后,它会发现/计算下一次运行的时间量。 The task then submits itself ( this ) to the passed executor service, along with the amount of time to elapse before the next task execution.然后任务将自身 ( this ) 提交给传递的执行程序服务,以及在下一个任务执行之前经过的时间量。

I have already posted Answers on Stack Overflow with code for tasks that re-schedule themselves.我已经在 Stack Overflow 上发布了 Answers,其中包含重新安排任务的代码。 I would expect others have as well.我希望其他人也有。 Search to learn more.搜索以了解更多信息。

Regarding the "countdown" aspect of your Question, read on.关于问题的“倒计时”方面,请继续阅读。

Countdown倒数

You have the right approach in using a scheduled executor service.您有使用预定执行程序服务的正确方法。 The problem is that you are calling the wrong method on that class.问题是你在那个 class 上调用了错误的方法。

Your call to schedule means you are scheduling several tasks to all run after a single second.您调用schedule意味着您正在安排几项任务在一秒钟后全部运行。 All those tasks are starting from the moment your call is made.所有这些任务都从您拨打电话的那一刻开始。 So each runs after one second from your call to schedule .因此,每个在您调用schedule后一秒后运行。 So the ten tasks are all waiting a second from almost the same moment: ten moments a split-second apart, the split-second being the time it takes for your for loop to continue.所以这十个任务几乎都在同一时刻等待一秒钟:十个时刻相隔一瞬间,这一瞬间是for循环继续所需的时间。

scheduleAtFixedRate

The method you are looking for is scheduleAtFixedRate .您正在寻找的方法是scheduleAtFixedRate To quote the doc:引用文档:

Submits a periodic action that becomes enabled first after the given initial delay, and subsequently with the given period;提交一个周期性动作,该动作在给定的初始延迟后首先启用,然后在给定的周期内启用; that is, executions will commence after initialDelay, then initialDelay + period, then initialDelay + 2 * period, and so on.也就是说,执行将在 initialDelay 之后开始,然后是 initialDelay + period,然后是 initialDelay + 2 * period,依此类推。

private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

public void countdown( ScheduledExecutorService scheduler ) 
{
    for ( int i = 1 ; i <= 10 ; i++ ) 
    {
        int countdownNumber =  10 - i ;  // For 9 through 0. Add 1 for 10 through 1. 
        scheduler.scheduleAtFixedRate 
        (
            () -> { System.out.println( countdownNumber ) ; } , 
            i ,  // 1 second, then 2 seconds, then 3 seconds, and so on to 10 seconds.
            TimeUnit.SECONDS 
        ) ;
    }
}

… Eventually shut down your scheduled executor service.

Notice how this approach does not require the ScheduledExecutorService to be single-threaded.请注意这种方法如何要求ScheduledExecutorService是单线程的。

Full example完整示例

Here is a complete example app.这是一个完整的示例应用程序。

package work.basil.example.countdown;

import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Countdown
{
    public static void main ( String[] args )
    {
        Countdown app = new Countdown();
        app.demo();
    }

    private void demo ( )
    {
        System.out.println( "INFO - Demo start. " + Instant.now() );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();  // Our code does *not* require the executor service to be single-threaded. But for this particular example, we might as well do it that way.
        this.countdown( scheduler );
        this.shutdownAndAwaitTermination( scheduler , Duration.ofMinutes( 1 ) , Duration.ofMinutes( 1 ) );

        System.out.println( "INFO - Demo end. " + Instant.now() );
    }

    public void countdown ( final ScheduledExecutorService scheduler )
    {
        Objects.requireNonNull( scheduler ) ; 

        for ( int i = 1 ; i <= 10 ; i++ )
        {
            int countdownNumber = 10 - i;  // For 9 through 0. Add 1 for 10 through 1.
            scheduler.scheduleAtFixedRate
            (
                    ( ) -> { System.out.println( "Countdown: " + countdownNumber + " at " + Instant.now() ); } ,
                    i ,  // 1 second, then 2 seconds, then 3 seconds, and so on to 10 seconds.
                    TimeUnit.SECONDS
            );
        }
    }

    // My slightly modified version of boilerplate code taken from Javadoc of `ExecutorService`.
    // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html
    void shutdownAndAwaitTermination ( final ExecutorService executorService , final Duration waitForWork , final Duration waitForRemainingTasks )
    {
        Objects.requireNonNull( executorService ) ;
        Objects.requireNonNull( waitForWork ) ;
        Objects.requireNonNull( waitForRemainingTasks ) ;

        executorService.shutdown(); // Disable new tasks from being submitted
        try
        {
            // Wait a while for existing tasks to terminate
            if ( ! executorService.awaitTermination( waitForWork.toMillis() , TimeUnit.MILLISECONDS ) )
            {
                executorService.shutdownNow(); // Cancel currently executing tasks
                // Wait a while for tasks to respond to being cancelled
                if ( ! executorService.awaitTermination( waitForRemainingTasks.toMillis() , TimeUnit.MILLISECONDS ) )
                { System.err.println( "ExecutorService did not terminate." ); }
            }
        }
        catch ( InterruptedException ex )
        {
            // (Re-)Cancel if current thread also interrupted
            executorService.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
        System.out.println( "DEBUG - shutdownAndAwaitTermination ran. " + Instant.now() );
    }
}

When run:运行时:

INFO - Demo start. 2023-01-20T21:24:47.379244Z
Countdown: 9 at 2023-01-20T21:24:48.390269Z
Countdown: 8 at 2023-01-20T21:24:49.390045Z
Countdown: 7 at 2023-01-20T21:24:50.389957Z
Countdown: 6 at 2023-01-20T21:24:51.386468Z
Countdown: 5 at 2023-01-20T21:24:52.390168Z
Countdown: 4 at 2023-01-20T21:24:53.386538Z
Countdown: 3 at 2023-01-20T21:24:54.387583Z
Countdown: 2 at 2023-01-20T21:24:55.386705Z
Countdown: 1 at 2023-01-20T21:24:56.389490Z
Countdown: 0 at 2023-01-20T21:24:57.387566Z
DEBUG - shutdownAndAwaitTermination ran. 2023-01-20T21:24:57.391224Z
INFO - Demo end. 2023-01-20T21:24:57.391966Z

By the way, know that scheduled tasks do not always fire exactly on time for a variety of reasons.顺便说一下,由于各种原因,预定任务并不总是准时触发。

Also, be aware that messages sent to System.out across threads do not always appear on the console chronologically.另外,请注意跨线程发送到System.out的消息并不总是按时间顺序显示在控制台上。 If you care about order, always include and study a timestamp such as Instant#now .如果您关心顺序,请始终包含并研究时间戳,例如Instant#now

You can schedule the next call inside the task.您可以在任务中安排下一次通话。

void countdown(final int i) {
    scheduler.schedule(() -> {
        System.out.println(i);
        if (i > 0) countdown(i - 1);
    }, 1, TimeUnit.SECONDS);
}
// ...
countdown(10);

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

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