July 20, 2010

Future Interrupted

I always thought of Java's Future interface as a fairly elegant and effective way of dealing with asynchronous tasks. It's lightweight, gives you a mechanism to check if a task is finished, allows you to block until the task completes and retrieve its result, and also provides a hook to attempt to cancel the task via interruption. When used in conjunction with the various thread pool services in java.util.concurrent package, it becomes a simple, yet powerful tool.

Yesterday, though, I discovered a subtle distinction about the semantic behavior of the Future interface that slightly diminishes its power. I found myself in a situation where I wanted to cancel a Future, but then still wait until its associated task has completed. Initially I thought this might be accomplished with something like this:

    // send an interrupt to the task's thread
    future.cancel(true);

    // block until the task is complete
    boolean finished = false;
    boolean interrupted = false;
    while (!finished) {
        try {
            future.get();
            finished = true;
        } catch (CancellationException ce) {
            // task has been cancelled, handle appropriately
            finished = true;
        } catch (ExecutionException ee) {
            // task completed with error, handle appropriately
            finished = true;
        } catch (InterruptedException ie) {
            // current thread has been interrupted, record and continue
            interrupted = true;
        }
    }
    // re-interrupt this thread if we've been interrupted
    if (interrupted) {
        Thread.currentThread().interrupt();
    }

Essentially, call cancel(true) on the Future in order to send an interrupt to the associated task's thread. Then call get() on the Future in a loop to ensure that the task has properly handled the interruption and completed execution before proceeding. However, the above code does not work as expected. The problem is that once you call cancel() on a Future, the task's thread will be sent an interrupt if it is running, but then the state associated with the task is completely cast aside. Subsequent calls to get() on the same Future will always immediately throw a CancellationException and there is no available mechanism to block until the task's thread has completed handling the interruption and finished its execution.

The worst part about this is that it is not immediately clear from the javadoc for the Future interface that this is the actual behavior. The description of the cancel() method is very clear that if the task has not started, it will prevent it from starting, but if it has already started, it can make an attempt to "cancel" it via interruption of the task's thread. Of course, since in Java interrupting a thread is only a suggestion for it to actually stop what it's doing, there's no guarantee about when or if the task will actually stop unless you handle the interruption properly. Therefore, I hoped that calling get() would wait for the computation to complete, and then throw a CancellationException if the thread was interrupted. After testing it and also digging through the Java source code for the Future implementation, I discovered that that is definitely not the case.

I struggled for a bit to come up with a reasonable alternative mechanism for accomplishing this behavior short of re-implementing my own thread pool and managing the threads myself. Fortunately, after speaking with a colleague, I was pointed to the beforeExecute and afterExecute hooks on the ThreadPoolExecutor. It takes a little more work then I had hoped, but using those hooks allows you to augment the functionality of ThreadPoolExecutor to achieve this type of behavior. I'll leave the details of it up to you.

Anyhow, that's my tidbit for the day. Be careful when playing with the Future!

1 comment :

  1. "I’ll leave the details of it up to you."

    What was your solution? Please share it.

    ReplyDelete