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
!
"I’ll leave the details of it up to you."
ReplyDeleteWhat was your solution? Please share it.