Cancelling tasks: Thread.interrupt() fragility
Posted by fabriziogiudici on March 2, 2009 at 9:58 AM EST
Today I reviewed some code in blueMarine as part of a moderate refactoring aimed at solving some problems with integration tests. This code is the portion that is triggered by a user selection (any thing of selection, it's an abstract class with polymorphic behaviour in implementations), scans the database in compliance with the user selection and provides the result to show in the viewer. It's the typical long task, so it uses multi-threading to avoid blocking the UI. Since the user might decide to go to another selection before the previous scan is over, a cancellation facility is needed to stop wasting CPU.This is the typical task for the concurrency API:
AtomicReference<Future<?>> runningTask = new AtomicReference<Future<?>>();
...
Future<?> t = runningTask.getAndSet(null);
if (t != null)
{
t.cancel(true);
}
ExecutorService executorService = Executors.newFixedThreadPool(4);
runningTask.set(executorService.submit(new Runnable()
{
public void run()
{
while (...)
{
...
if (thread.isInterrupted()) // this indeed is performed by another called method in a different class
{
break;
}
}
runningTask.set(null);
}
}));
The key of everything is the Future.cancel(true) method that will call Thread.interrupt() on the related thread. So, apparently the only thing that we should do is periodically poll for the isInterrupted() method and eventually stop our computation. Often this is performed by throwing an InterruptedException.
Now, there are two ways for querying a thread to see whether is interrupted; the latter way is to call Thread.interrupted(). This version will reset the interrupted flag; in other words, any further probe for the interruption will return false. If an exception is thrown, the catch code is given the chance to set the flag back to true.
So, it boils down to not using interrupted() in favour of isInterrupted(), right? Well, today's tests demonstrated that I was able to cancel a previous running task only once out of a dozen runs (and probably less). I can only think that my code calls some library (NetBeans stuff in this case) that potentially calls interrupted() without throwing an InterruptedException. So my code never understands that the thread is running in has been interrupted.
I'm currently investigating where the problem is - but, indeed, I came to the conclusion that the Thread.interrupt() mechanism is too fragile. This section is a part of blueMarine that is likely to be expanded (for different kinds of queries) and there's potentially a lot of code that could be called in that thread, potentially by third parties. Having the code to respect the Thread.interrupted() stuff seems too a heavy prerequisite for the contract.
So I decided to find a different, reliable and controllable solution. Of course, what I need is just a boolean flag to tell the code that the processing should be interrupted; it's just a matter of decoupling. The above code doesn't know which class will implement the processing method and vice versa. So I've ended up with a solution based on ThreadLocal:
public final class CancellationController
{
private static final ThreadLocal
{
@Override
protected CancellationController initialValue()
{
return new CancellationController(Thread.currentThread());
}
};
private volatile boolean cancelled = false;
@Nonnull
private final Thread thread;
private CancellationController (@Nonnull final Thread thread)
{
this.thread = thread;
}
public static boolean isCancelled()
{
return INSTANCE.get().cancelled;
}
@Override
@Nonnull
public String toString()
{
return String.format("CancellationController[%s]", thread.getName());
}
protected void cancel()
{
thread.interrupt();
cancelled = true;
}
@Nonnull
protected static CancellationController get()
{
return INSTANCE.get();
}
protected void reset()
{
cancelled = false;
}
}
The original code now is:
AtomicReference<CancellationController> runningTask = new AtomicReference<CancellationController>();
...
CancellationController t = runningTask.getAndSet(null);
if (t != null)
{
t.cancel();
}
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(new Runnable())
{
public void run()
{
CancellationController.get().reset();
runningTask.set(CancellationController.get());
while (...)
{
...
if (CancellationController.get().isCancelled())
{
break;
}
}
runningTask.set(null);
}
});
I suspect it's a recurrent problem so I'd like to know whether others are doing the same, or whether there are simpler solutions. Thanks.

Blog Links >>
- Login or register to post comments
- Printer-friendly version
- fabriziogiudici's blog
- 2649 reads






Comments
by haraldk - 2009-03-05 10:22
You'll still have issues with library code that depends on other broken library code, so there is still no guarantee that your tasks will cancel quickly as they should. I guess there is really no excuse for not doing the right thing. But at least you are not making the situation worse this way..by tackline - 2009-03-04 13:53
Thread.interrupt is nasty. It can even mess up loading classes, and therefore make a class loader unusable. It's not so much that the API is bad, but the concept is poor.by cowwoc - 2009-03-03 20:41
Agreed. Thread.interrupted() is a bad API. Very bad. Sun should deprecate it immediately and replace it with something more reliable (don't reset and get using the same method!). Just my 2 cents.by i30817 - 2009-03-03 13:23
The alternative is using the old input/output streams that most emphatically doesn't interrupt the task (oh asynchronous exceptions how i love thees. Not). I had to make my own input streams because of this (not my library code). I hate when my code depends on if some library broke an obscure contract or not. I mean i do it all the time (unknowingly)!by mthornton - 2009-03-03 11:16
Another problem with interrupt is that when using nio an interrupt not only ends the operation in progress but also closes the channel. This may leave the content of a file in an inconsistent state.by fabriziogiudici - 2009-03-02 19:18
Thanks. It got lost manually copying from the code.by simonebordet - 2009-03-02 18:36
CancellationController.cancelled must be volatile.by fabriziogiudici - 2009-03-02 18:09
Seems fine now.by fabriziogiudici - 2009-03-02 18:05
Grrr... it ate all the generics angle brackes. Trying to fixing it.