简体   繁体   中英

Is there a way to schedule a task until an event occurs or a specific amount of time passes?

Let's say I have a task that will check if a certain condition has been met. If that condition has been met, I should terminate the task early. However, I should continue to run the task for N number of minutes while continuously checking if that condition has been met. At the end of N minutes, the task should terminate itself.

I've been looking into Timer, TimerTask, and ExecutorService, but none of them seem to offer the type of solution I am looking for. For example, these approaches will allow you to schedule a task to run once, or repeatedly, but not for a specific amount of time (aka N minutes).

ScheduledExecutorService can accomplish this.

What you need to do:

  • Schedule the block of work you want to do at a fixed interval (in time units, so like seconds, minutes, hours, etc)
  • When you've completed the work, execute ScheduledExecutorService#shutdownNow
  • To time out after a period of time, use ScheduledExecutorService#awaitTermination to delay the termination of the scheduled task until your threshold elapses

Task reschedules itself

The Answer by Makato is right about suggesting the use of an executor service . But ending the scheduled executor service as the way to end your repeating task is less than optimal. Often we have one executor service handling many tasks. So while we want our repeating task to end, the executor service may need to continue scheduling other tasks to run.

Instead, ironically, do not schedule the repeating task to repeat . Schedule the task to run once . Have the task check its status.

  • If the status requires the task run again, have the task schedule itself on the executor service for another run.
  • If the status is complete, and no further executions of that task are needed, simply opt out of another submission to the executor service.

The key to making this work is passing a reference to the scheduled executor service to the constructor of your task . The task keeps that reference as a member field of its own class. So the task can use that reference to pass itself to the scheduled executor service for another submission. The task becomes self-scheduling.

Here is the source code for an example of such a task.

Notice that in addition to passing a reference to the scheduled executor service, we also pass an Instant representing our target moment when we want to stop our repeating task. And we pass a Duration object for the amount of time to wait between executions of this task.

Examine that run method. The task compares the current moment against the target moment. If not yet passed, the task schedules it own next execution. If passed, no rescheduling, mission accomplished.

package work.basil.example.concurrency;

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

public class ExpiringTask implements Runnable
{
    private final Instant timeLimit;
    private final Duration untilNextRun;
    private final ScheduledExecutorService scheduledExecutorService;

    public ExpiringTask ( final Instant timeLimit , final Duration untilNextRun , final ScheduledExecutorService scheduledExecutorService )
    {
        this.timeLimit = Objects.requireNonNull( timeLimit );
        this.untilNextRun = Objects.requireNonNull( untilNextRun );
        this.scheduledExecutorService = Objects.requireNonNull( scheduledExecutorService );
    }

    @Override
    public void run ( )
    {
        System.out.println( "INFO - Running the `run` method of our ExpiringTask. Now: " + Instant.now() );
        if ( Instant.now().isAfter( this.timeLimit ) )
        {
            System.out.println( "☑️ Mission accomplished. Our task has expired. Now: " + Instant.now() );
        }
        else
        {
            System.out.println( "⏲️ Not yet expired. Reschedule to check again. Now: " + Instant.now() );
            this.scheduledExecutorService.schedule( this , this.untilNextRun.toNanos() , TimeUnit.NANOSECONDS );
        }
    }
}

In real work, when rescheduling the task, I would check that the executor service is still available. To do so, make a call to ExecutorSerivce#isShutdown . This step is omitted in code above for brevity and simplicity.

How to run get that task running? Here is the main chunk of an app to demonstrate this task running repeatedly while rescheduling itself.

First we establish our target time limit as five seconds from now. Then we specify waiting between runs for one second each time. And we get a scheduled executor service. Be sure to keep that reference around, as you must be careful to gracefully shut down the executor service eventually — otherwise its backing thread pool may continue running indefinitely, like a zombie ♂️.

Next step is to instantiate our task, passing the pieces to its constructor. Finally, to get things working, we tell the scheduled executor service to run our task after a certain amount of delay. For this first run we want to run immediately, so we specify a delay of zero nanoseconds.

Instant timeLimit = Instant.now().plus( Duration.ofSeconds( 5 ) );
Duration untilNextRun = Duration.ofSeconds( 1 );
ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
ExpiringTask task = new ExpiringTask( timeLimit , untilNextRun , ses );
ses.schedule( task , 0 , TimeUnit.NANOSECONDS );

That covers the main idea. Here is the complete app class containing that code excerpted directly above.

package work.basil.example.concurrency;

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

public class KeepRunningUntil
{
    public static void main ( String[] args )
    {
        KeepRunningUntil app = new KeepRunningUntil();
        app.demo();
        System.out.println( "INFO - Demo ending. Now: " + Instant.now() );
    }

    private void demo ( )
    {
        System.out.println( "INFO - 'demo' method starting. " + Instant.now() );

        Instant timeLimit = Instant.now().plus( Duration.ofSeconds( 5 ) );
        Duration untilNextRun = Duration.ofSeconds( 1 );
        ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
        ExpiringTask task = new ExpiringTask( timeLimit , untilNextRun , ses );
        ses.schedule( task , 0 , TimeUnit.NANOSECONDS );

        // Wait a while for our background task to do its thing.
        try { Thread.sleep( Duration.ofSeconds( 10 ).toMillis() ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
        this.shutdownAndAwaitTermination( ses );

        System.out.println( "INFO - 'demo' method ending. " + Instant.now() );
    }

    private void shutdownAndAwaitTermination ( ExecutorService executorService )
    {
        // Boilerplate taken from Javadoc.
        // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html#isShutdown()

        executorService.shutdown(); // Disable new tasks from being submitted
        try
        {
            // Wait a while for existing tasks to terminate
            if ( ! executorService.awaitTermination( 60 , TimeUnit.SECONDS ) )
            {
                executorService.shutdownNow(); // Cancel currently executing tasks
                // Wait a while for tasks to respond to being cancelled
                if ( ! executorService.awaitTermination( 60 , TimeUnit.SECONDS ) )
                { System.err.println( "Pool did not terminate" ); }
            }
        }
        catch ( InterruptedException ex )
        {
            // (Re-)Cancel if current thread also interrupted
            executorService.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
    }
}

When run.

INFO - 'demo' method starting. 2022-08-25T08:16:28.384638Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:28.402471Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:28.402657Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:29.408010Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:29.408126Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:30.413336Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:30.413599Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:31.416144Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:31.416230Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:32.421484Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:32.421873Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:33.425519Z
☑️ Mission accomplished. Our task has expired. Now: 2022-08-25T08:16:33.425863Z
INFO - 'demo' method ending. 2022-08-25T08:16:38.407566Z
INFO - Demo ending. Now: 2022-08-25T08:16:38.407755Z

The other answers mentioned here are good. Just giving another idea that might be simpler.

  1. Schedule your task to run periodically and don't worry about termination
  2. Create a class Lets call it TaskManager and make one of its members an ExecutorService . Pass the ExecutorService that you use for your task sceduling to your TaskManager . Also in your task manager add a counter that your task will increment each time that it runs. Obviously your TaskManager should have a static method incrementCounter() that your task will use. Also your TaskManager should have a method static method teminateTask() .
  3. So now when the counter reaches your limit TaskManager will call method shutDown() of your ExecutorService . Or if your task in your internal logic comes to conclusion that its time to terminate it will call method teminateTask() of your TaskManager

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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