Aborting and Interrupting Threads

There are two methods in the Thread class which are often used for stopping threads - Abort and Interrupt. I don't recommend using either of these methods, but it's worth knowing about them - if only for why they should almost always be avoided.

Aborting a thread

Calling Thread.Abort aborts that thread as soon as possible. (Aborting a thread which is executing unmanaged code has no effect until the CLR gets control again.) A ThreadAbortException is thrown, which is a special exception which can be caught, but will automatically be rethrown at the end of the catch block. As it keeps being thrown, the exception will usually terminate the thread. Thread.ResetAbort can be called (if the caller has appropriate permissions) to stop the thread's abortion. Calling the method doesn't prevent stop the currently thrown exception, it just stops the exception from being rethrown at the end of a catch block. (Usually this distinction is irrelevant, as you'd almost always want to call ResetAbort from a catch block anyway).

Interrupting a thread

Calling Thread.Interrupt is similar, but somewhat less drastic. This causes a ThreadInterruptedException exception to be thrown the next time the thread enters the WaitSleepJoin state, or immediately if the thread is already in that state. There's nothing particularly special about the ThreadInterruptedException - it doesn't get rethrown like ThreadAbortException does. Note that threads can block without entering the WaitSleepJoin state, however. For example, reading from a blocking stream (a common situation where you'd like to interrupt a thread) doesn't make the thread enter the WaitSleepJoin state.

A nasty bug in the framework...

A post on a newsgroup drew my attention to a bug in version 1.0/1.1 of the framework. If a thread is aborted or interrupted while it is calling Monitor.Wait, and after the monitor has been pulsed but before the thread has been able to acquire it again, it returns from the call (with the appropriate exception) without reacquiring the monitor. This can lead to situations where the code makes it look like you'll certainly own the monitor, but you're executing it after an abort or interrupt and you no longer own it. Here's an example:

using System;
using System.Threading;

class Test
{
                  static object someLock = new object();
    
                  static void Main()
    {
        Console.WriteLine ("Main thread starting");
        Thread secondThread = new Thread (new ThreadStart(ThreadJob));
        secondThread.Start();
        Console.WriteLine ("Main thread sleeping");
        Thread.Sleep(500);
                  lock (someLock)
        {
            Console.WriteLine ("Main thread acquired lock - pulsing monitor");
            Monitor.Pulse(someLock);
            Console.WriteLine ("Monitor pulsed; interrupting second thread");
            secondThread.Interrupt();
            Thread.Sleep(1000);
            Console.WriteLine ("Main thread still owns lock...");
        }
    }
    
                  static void ThreadJob()
    {
        Console.WriteLine ("Second thread starting");
        
                  lock (someLock)
        {
            Console.WriteLine ("Second thread acquired lock - about to wait");
                  try
            {
                Monitor.Wait(someLock);
            }
                  catch (Exception e)
            {
                Console.WriteLine ("Second thread caught an exception: {0}", e.Message);
            }
        }
    }
}

The results of the above are:

Main thread starting
Main thread sleeping
Second thread starting
Second thread acquired lock - about to wait
Main thread acquired lock - pulsing monitor
Monitor pulsed; interrupting second thread
Second thread caught an exception: Thread has been interrupted from a waiting state.
Main thread still owns lock...

Note the order of the last two lines - the line from the second thread has been written while the main thread owns the lock, despite being within the second thread's lock block.

In fact, the above code should throw a SynchronizationLockException when it implicitly calls Monitor.Exit at the end of the lock block. As it happens, Monitor.Exit doesn't throw the exception (despite the documentation's claims to the contrary).

It's hard to know exactly what should really happen here - if you've told a thread to be interrupted or aborted, you probably don't want it to hang around for a long time trying to reacquire a monitor (which is exactly what it does if the monitor hasn't been pulsed before the thread is interrupted). On the other hand, not being able to rely on a lock actually being owned within a lock block is nasty. I believe the behaviour has been changed for version 2 of the framework, but we'll have to wait to see exactly how it's changed.

Why Thread.Abort/Interrupt should be avoided

I don't use Thread.Abort/Interrupt routinely. I prefer a graceful shutdown which lets the thread do anything it wants to, and keeps things orderly. I dislike aborting or interrupting threads for the following reasons:

They aren't immediate
One of the reasons often given for not using the graceful shutdown pattern is that a thread could be waiting forever. Well, the same is true if you abort or interrupt it. If it's waiting for input from a stream of some description, you can abort or interrupt the thread and it will go right on waiting. If you only interrupt the thread, it could go right on processing other tasks, too - it won't actually be interrupted until it enters the WaitSleepJoin state.
They can't be easily predicted
While they don't happen quite as quickly as you might sometimes want, aborts and interrupts do happen where you quite possibly don't want them to. If you don't know where a thread is going to be interrupted or aborted, it's hard to work out exactly how to get back to a consistent state. Although finally blocks will be executed, you don't want to have to put them all over the place just in case of an abort or interrupt. In almost all cases, the only time you don't mind a thread dying at any point in its operation is when the whole application is going down.
The bug described above
Getting your program into an inconsistent state is one problem - getting it into a state which, on the face of it, shouldn't even be possible is even nastier.

Next page: Collected Hints and Tips
Previous page: Thread-safe Types and Methods

Back to the main C# page.