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.
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).
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 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.
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:
WaitSleepJoin
state.
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.
Back to the main C# page.