简体   繁体   中英

Future and ExecutorService, how to know when a cancelled task has terminated?

Suppose I have some code that start a task using an ExecutorService and then the invoker cancels it, by means of the Future returned by the submit() method:

execService = Executors.newSingleThreadExecutor ();
Future<String> result = execService.submit ( () -> {
  for ( ... ) {
    if ( Thread.interrupted() ) break;
    // Stuff that takes a while
  }
  return "result";
});
...
result.cancel ( true );
// I'm not sure the corresponding thread/task has finished and the call() method 
// above returned
// result.isDone() is immediately true. result.get() throws CancellationException

So, I can cancel the background task and make the executor ready to start again. However, I cannot do the latter before the interrupted task completes the interruption and the corresponding method returns.

Note that in the code above the main flow returns straight after result.cancel() and result.isCancelled() is true straight after that, at the same time, the parallel task might take still a while before checking Thread.interrupted() again and terminate. I need to be sure that the side task is completely finished before continuing in the main thread.

Moreover, note that the use of a single thread executor is accidental, apart from this simple example, I'd like to solve the problem for both the case of just one parallel thread and for the one where more than one thread are running.

What's the best way to be sure of that?

So far I've thought of either introducing a flag that is visible to both the task and the invoker, or shutting down the executor and waiting for that to complete. The latter solution might be inefficient if one wants to reuse the executor many other times, the former is better, but I'd like to know if there is another simpler or more canonical way.

I also struggled with a similar problem, I ended up making this utility:

    private enum TaskState {
        READY_TO_START,
        RUNNING,
        DONE,
        MUST_NOT_START;
    }
    
    /**
     * Runs the given set of tasks ensuring that:
     * 
     * If one fails this will not return while tasks are running and once returned
     * no more tasks will start.
     * 
     * 
     * @param <V>
     * @param pool
     * @param tasksToRun
     */
    public <V> void runAndWait(ExecutorService pool, List<Callable<V>> tasksToRun) {
        
        // We use this to work around the fact that the future doesn't tell us if the task is actually terminated or not.
        List<AtomicReference<TaskState>> taskStates = new ArrayList<>();
        List<Future<V>> futures = new ArrayList<>();
        
        for(Callable<V> c : tasksToRun) {
            AtomicReference<TaskState> state = new AtomicReference<>(TaskState.READY_TO_START);
            futures.add(pool.submit(new Callable<V>() {

                @Override
                public V call() throws Exception {
                    if(state.compareAndSet(TaskState.READY_TO_START, TaskState.RUNNING)) {
                        try {
                            return c.call();
                        } finally {
                            state.set(TaskState.DONE);
                        }
                    } else {
                        throw new CancellationException();
                    }
                }
                
            }));
            taskStates.add(state);
        }
        int i = 0;
        try {
            
            // Wait for all tasks.
            for(; i < futures.size(); i++) {
                futures.get(i).get(7, TimeUnit.DAYS);
            }
        } catch(Throwable t) { 
            try {
                // If we have tasks left that means something failed.
                List<Throwable> exs = new ArrayList<>();
                final int startOfRemaining = i;
                
                // Try to stop all running tasks if any left.
                for(i = startOfRemaining; i < futures.size(); i++) {
                    try {
                        futures.get(i).cancel(true);
                    } catch (Throwable e) {
                        // Prevent exceptions from stopping us from
                        // canceling all tasks. Consider logging this.
                    }
                }
                
                // Stop the case that the task is started, but has not reached our compare and set statement. 
                taskStates.forEach(s -> s.compareAndSet(TaskState.READY_TO_START, TaskState.MUST_NOT_START));
                
                for(i = startOfRemaining; i < futures.size(); i++) {
                    try {
                        futures.get(i).get();
                    } catch (InterruptedException e) {
                        break; // I guess move on, does this make sense should we instead wait for our tasks to finish?
                    } catch (CancellationException e) {
                        // It was cancelled, it may still be running if it is we must wait for it.
                        while(taskStates.get(i).get() == TaskState.RUNNING) {
                            Thread.sleep(1);
                        }
                    } catch (Throwable t1) {
                        // Record the exception if it is interesting, so users can see why it failed.
                        exs.add(t1);
                    }
                }
                
                exs.forEach(t::addSuppressed);
            } finally {
                throw new RuntimeException(t);
            }
        }
    }

I ended up wrapping each task, in something so I could know if all tasks were done.

Submit all tasks that you need to execute. Since you created a single-thread executor, at any time there will be not more than one task running. They will be executed subsequently , one will be started only after the previous one ended (completed or was interrupted). You can imagine that like a queue.

You code may look as follows:

Future<String> result1 = execService.submit(task-1);
Future<String> result2 = execService.submit(task-2);
Future<String> result3 = execService.submit(task-3);

Update 1

You asked: I need to be sure that the side task is completely finished before continuing in the main thread. This exactly how executor works. No matter what one tasks is doing - executing its normal logic or handling an interrupt request - the thread is busy and all other tasks are waiting for it.

To your question I'm interested in the general case : Tell us how do you define general . Do you mean that the number of threads can be more than 1? Do you mean that any task can depend on any other task*? Do you mean that the logic what task should be started depends on results of execution of other tasks? Etc.

Executors in Java have the purpose to encapsulate the overhead related to creating and monitoring threads. Using executors only very simple workflows can be implemented. If you want to implement some complex logic, then executors are no the best choice.

Instead, consider using workflow engines like Activity, Camunda or Bonita. Then you will be able to execute some tasks simultaneously, you will be able to specify that before task X starts, it should wait until simultaneously running tasks A, B and C are all completed, etc.

Update 2

In your code you have

if (Thread.interrupted()) ...

But this checks your main thread, not the thread where the executor is executing your task.

If you want to know how the task was executed - has it completed its whole work, was there any exception (like NullPOinterException),or was it interrupted - then you should use other approach, like following:

Future<String> future1 = execService.submit(task1);

try {
  String result1 = future1.get();

  if (future1.isCancelled()) {
    // Your reaction on cancellation
    ...
  } else {
    // If we are here, it means task has completed its whole work:
    // - Task was not cancelled
    // - Task thread was not interrupted
    // - There was no unhandled exception
    ...
  }
} catch (InterruptedException ie) {
  // Your reaction on interruption.
  ...
} catch (ExecutionException ee) {
  // Your reaction on unhandled exception in the task
  ...
}

If you have such logic after each task, the code can be pretty complicated. It may be difficult to understand such code one month after you implemented it:)

Do you really have such complex logic so that one task should analyze the result of execution of the previous task? Do you want to decide what task to start as next depending on the result of execution of the previous task?

If the only thing you want to be sure is that the previous task has completed all its activities - its normal work, its exception handling, its reaction on thread interruption etc. - then again you have this in the single thread executor. Because executor does this all for you.

May be your expectation how executor works is not 100% correct. Just do a small prototype with single thread executor including interruption and tell us why it doesn't fit your case.

As per Future.cancel(boolean) documentation:

Attempts to cancel execution of this task. This attempt will fail if the task has already completed, has already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.

After this method returns, subsequent calls to isDone() will always return true. Subsequent calls to isCancelled() will always return true if this method returned true.

As per get() documentation:

V get() throws InterruptedException, ExecutionException Waits if necessary for the computation to complete, and then retrieves its result.

Suppose we've executed a task but, for good reason, we don't care about the result anymore. We can use Future.cancel(boolean) to tell the executor to stop the operation and interrupt its underlying thread:

future.cancel(true);

Our instance of Future from the code above would never complete its operation.

In fact, if we try to call get() from that instance, after the call to cancel(), the outcome would be a CancellationException.

Future.isCancelled() will tell us if a Future was already canceled. This can be quite useful to avoid getting a CancellationException.

It is possible that a call to cancel() fails. In that case, its returned value will be false. Notice that cancel() takes a boolean value as an argument – this controls whether the thread executing this task should be interrupted or not.

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