2
votes

We are using the TPL to queue long-running tasks into the threadpool. Some of the tasks can block for some time, so we are using the following pattern to cancel them:

private void RunAction(Action action, CancellationTokenSourceWithException cts)
{

    try
    {
        s_logger.Info("Starting action on thread ID: {0}", Utils.GetCurrentNativeThreadId());

        Thread taskThread = Thread.CurrentThread;
        cts.Token.Register(() => InterruptTask(taskThread));

        s_logger.Info("Running next action");
        action();
    }
    catch (Exception e)
    {
        cts.Cancel(e);
        throw;
    }

This way, calling cts.Cancel() will cause the task thread to be interrupted in case it is blocking. This, however, has led to a problem: we don't know if the thread actually got the ThreadInterruptedException or not. It is possible that we call Thread.Interrupt() on it, but the thread will run to completion and the task will simply end. In that case, the threadpool thread will have a ticking bomb in the form of the ThreadInterruptedException, and whenver another task runs on this thread and attempts to block, it will get this exception.

A Thread.ResetInterrupted() method (similar to Thread.ResetAbort()) would be helpful here, but it does not seem to exist. We can use something like the following:

try
{
    someEvent.Wait(10);
}
catch (ThreadInterruptedException) {}

To swallow the ThreadInterruptedException, but it looks ugly.

Can anyone suggest an alternative? Are we wrong to be calling Thread.Interrupt on threadpool threads? It seems like the easiest way to cancel tasks: cooperative cancellation using events etc. are much more cumbersome to use, and have to propagate into all classes that we use from the task.

2

2 Answers

5
votes

You cannot do this because you don't know if/when the thread pool's threads will block when not running your own code!

Apart from the problems you mentioned, if a thread decides to block while not running your own code then the ThreadInterruptException will be unhandled and the app will immediately terminate. This is something you cannot work around with a try/block/catch guard because there is a race condition: the guard might have just completed when Thread.Interrupt is called, so if the runtime decides to have the thread block at that point you 'll get a crash.

So using Thread.Interrupt is not a viable option and you will definitely have to set up cooperative cancellation.

Apart from that, you should probably not be using the thread pool for these tasks in the first place (although there's not enough data to be . Quoting the docs (emphasis mine):

If you have short tasks that require background processing, the managed thread pool is an easy way to take advantage of multiple threads.

There are several scenarios in which it is appropriate to create and manage your own threads instead of using thread pool threads:

  • ...
  • You have tasks that cause the thread to block for long periods of time. The thread pool has a maximum number of threads, so a large number of blocked thread pool threads might prevent tasks from starting.
  • ...

You might therefore want to consider using a thread pool of your own (there is an apparently very reputable implementation here).

1
votes

Simple. You need to pass a CancellationToken to the action being called and act on it when cancellation is signalled. Messing with TPL threads with Interrupt is definitely the wrong action to take and will leave TPL in a "confused" state. Adopt the cancellation pattern all the way.