(Post 14/12/2007) Calling Abort on one's own
thread is one circumstance in which Abort is totally safe. Another is
when you can be certain the thread you're aborting is in a particular
section of code, usually by virtue of a synchronization mechanism such
as a Wait Handle or Monitor.Wait. A third instance in which calling Abort
is safe is when you subsequently tear down the thread's application domain
or process.
A thread can be ended forcibly via the Abort method:
class Abort {
static void Main() {
Thread t = new Thread (delegate() {while(true);}); // Spin forever
t.Start();
Thread.Sleep (1000); // Let it run for a second...
t.Abort(); // then abort it.
}
}
The thread upon being aborted immediately enters the
AbortRequested state. If it then terminates as expected, it goes into
the Stopped state. The caller can wait for this to happen by calling Join:
class Abort {
static void Main() {
Thread t = new Thread (delegate() { while (true); });
Console.WriteLine (t.ThreadState); // Unstarted
t.Start();
Thread.Sleep (1000);
Console.WriteLine (t.ThreadState); // Running
t.Abort();
Console.WriteLine (t.ThreadState); // AbortRequested
t.Join();
Console.WriteLine (t.ThreadState); // Stopped
}
}
Abort causes a ThreadAbortException to be thrown on the
target thread, in most cases right where the thread's executing at the
time. The thread being aborted can choose to handle the exception, but
the exception then gets automatically re-thrown at the end of the catch
block (to help ensure the thread, indeed, ends as expected). It is, however,
possible to prevent the automatic re-throw by calling Thread.ResetAbort
within the catch block. Then thread then re-enters the Running state (from
which it can potentially be aborted again). In the following example,
the worker thread comes back from the dead each time an Abort is attempted:
class Terminator {
static void Main() {
Thread t = new Thread (Work);
t.Start();
Thread.Sleep (1000); t.Abort();
Thread.Sleep (1000); t.Abort();
Thread.Sleep (1000); t.Abort();
}
static void Work() {
while (true) {
try { while (true); }
catch (ThreadAbortException) { Thread.ResetAbort(); }
Console.WriteLine ("I will not die!");
}
}
}
ThreadAbortException is treated specially by the runtime,
in that it doesn't cause the whole application to terminate if unhandled,
unlike all other types of exception.
Abort will work on a thread in almost any state – running,
blocked, suspended, or stopped. However if a suspended thread is aborted,
a ThreadStateException is thrown – this time on the calling thread – and
the abortion doesn't kick off until the thread is subsequently resumed.
Here's how to abort a suspended thread:
try { suspendedThread.Abort();
}
catch (ThreadStateException) { suspendedThread.Resume(); }
// Now the suspendedThread will abort.
Complications with Thread.Abort
Assuming an aborted thread doesn't call ResetAbort, one
might expect it to terminate fairly quickly. But as it happens, with a
good lawyer the thread may remain on death row for quite some time! Here
are a few factors that may keep it lingering in the AbortRequested state:
- Static class constructors are never aborted part-way through (so
as not to potentially poison the class for the remaining life of the
application domain)
- All catch/finally blocks are honored, and never aborted mid-stream
- If the thread is executing unmanaged code when aborted, execution
continues until the next managed code statement is reached
The last factor can be particularly troublesome, in that
the .NET framework itself often calls unmanaged code, sometimes remaining
there for long periods of time. An example might be when using a networking
or database class. If the network resource or database server dies or
is slow to respond, it's possible that execution could remain entirely
within unmanaged code, for perhaps minutes, depending on the implementation
of the class. In these cases, one certainly wouldn't want to Join the
aborted thread – at least not without a timeout!
Aborting pure .NET code is less problematic, as long
as try/finally blocks or using statements are incorporated to ensure proper
cleanup takes place should a ThreadAbortException be thrown. However,
even then, one can still be vulnerable to nasty surprises. For example,
consider the following:
using (StreamWriter w = File.CreateText
("myfile.txt"))
w.Write ("Abort-Safe?");
C#'s using statement is simply a syntactic shortcut,
which in this case expands to the following:
StreamWriter w;
w = File.CreateText ("myfile.txt");
try { w.Write ("Abort-Safe"); }
finally { w.Dispose(); }
It's possible for an Abort to fire after the StreamWriter
is created, but before the try block begins. In fact, by digging into
the IL, one can see that it's also possible for it to fire in between
the StreamWriter being created and assigned to w:
IL_0001: ldstr "myfile.txt"
IL_0006: call class [mscorlib]System.IO.StreamWriter
[mscorlib]System.IO.File::CreateText(string)
IL_000b: stloc.0
.try
{
...
Either way, the Dispose method in the finally block is
circumvented, resulting in an abandoned open file handle – preventing
any subsequent attempts to create myfile.txt until the application domain
ends.
In reality, the situation in this example is worse still,
because an Abort would most likely take place within the implementation
of File.CreateText. This is referred to as opaque code – that which we
don't have the source. Fortunately, .NET code is never truly opaque: we
can again wheel in ILDASM, or better still, Lutz Roeder's Reflector –
and looking into the framework's assemblies, see that it calls StreamWriter's
constructor, which has the following logic:
public StreamWriter (string
path, bool append, ...)
{
...
...
Stream stream1 = StreamWriter.CreateFile (path, append);
this.Init (stream1, ...);
}
Nowhere in this constructor is there a try/catch block,
meaning that if the Abort fires anywhere within the (non-trivial) Init
method, the newly created stream will be abandoned, with no way of closing
the underlying file handle.
Because disassembling every required CLR call is obviously
impractical, this raises the question on how one should go about writing
an abort-friendly method. The most common workaround is not to abort another
thread at all – but rather add a custom boolean field to the worker's
class, signaling that it should abort. The worker checks the flag periodically,
exiting gracefully if true. Ironically, the most graceful exit for the
worker is by calling Abort on its own thread – although explicitly throwing
an exception also works well. This ensures the thread's backed right out,
while executing any catch/finally blocks – rather like calling Abort from
another thread, except the exception is thrown only from designated places:
class ProLife {
public static void Main() {
RulyWorker w = new RulyWorker();
Thread t = new Thread (w.Work);
t.Start();
Thread.Sleep (500);
w.Abort();
}
public class RulyWorker {
// The volatile keyword ensures abort is not cached by a thread
volatile bool abort;
public void Abort() { abort = true; }
public void Work() {
while (true) {
CheckAbort();
// Do stuff...
try { OtherMethod(); }
finally { /* any required cleanup */ }
}
}
void OtherMethod() {
// Do stuff...
CheckAbort();
}
void CheckAbort() { if (abort) Thread.CurrentThread.Abort(); }
}
}
Calling Abort on one's
own thread is one circumstance in which Abort is totally safe. Another
is when you can be certain the thread you're aborting is in a particular
section of code, usually by virtue of a synchronization mechanism
such as a Wait Handle or Monitor.Wait. A third instance in which
calling Abort is safe is when you subsequently tear down the thread's
application domain or process. |
Ending Application Domains
Another way to implement an abort-friendly worker is
by having its thread run in its own application domain. After calling
Abort, one simply tears down the application domain, thereby releasing
any resources that were improperly disposed.
Strictly speaking, the first step – aborting the thread – is unnecessary,
because when an application domain is unloaded, all threads executing
code in that domain are automatically aborted. However, the disadvantage
of relying on this behavior is that if the aborted threads don't exit
in a timely fashion (perhaps due to code in finally blocks, or for other
reasons discussed previously) the application domain will not unload,
and a CannotUnloadAppDomainException will be thrown on the caller. For
this reason, it's better to explicitly abort the worker thread, then call
Join with some timeout (over which you have control) before unloading
the application domain.
In the following example, the worker enters an infinite
loop, creating and closing a file using the abort-unsafe File.CreateText
method. The main thread then repeatedly starts and aborts workers. It
usually fails within one or two iterations, with CreateText getting aborted
part way through its internal implementation, leaving behind an abandoned
open file handle:
using System;
using System.IO;
using System.Threading;
class Program {
static void Main() {
while (true) {
Thread t = new Thread (Work);
t.Start();
Thread.Sleep (100);
t.Abort();
Console.WriteLine ("Aborted");
}
}
static void Work() {
while (true)
using (StreamWriter w = File.CreateText ("myfile.txt")) { }
}
}
Aborted
Aborted
IOException: The process cannot access the file 'myfile.txt' because
it
is being used by another process. |
Here's the same program modified so the worker thread
runs in its own application domain, which is unloaded after the thread
is aborted. It runs perpetually without error, because unloading the application
domain releases the abandoned file handle:
class Program {
static void Main (string [] args) {
while (true) {
AppDomain ad = AppDomain.CreateDomain ("worker");
Thread t = new Thread (delegate() { ad.DoCallBack (Work); });
t.Start();
Thread.Sleep (100);
t.Abort();
if (!t.Join (2000)) {
// Thread won't end - here's where we could take further action,
// if, indeed, there was anything we could do. Fortunately in
// this case, we can expect the thread *always* to end.
}
AppDomain.Unload (ad); // Tear down the polluted domain!
Console.WriteLine ("Aborted");
}
}
static void Work() {
while (true)
using (StreamWriter w = File.CreateText ("myfile.txt")) { }
}
}
Aborted
Aborted
Aborted
Aborted
...
... |
Creating and destroying an application domain is classed
as relatively time-consuming in the world of threading activities (taking
a few milliseconds) so it's something conducive to being done irregularly
rather than in a loop! Also, the separation introduced by the application
domain introduces another element that can be either of benefit or detriment,
depending on what the multi-threaded program is setting out to achieve.
In a unit-testing context, for instance, running threads on separate application
domains can be of great benefit.
Ending Processes
Another way in which a thread can end is when the parent
process terminates. One example of this is when a worker thread's IsBackground
property is set to true, and the main thread finishes while the worker
is still running. The background thread is unable to keep the application
alive, and so the process terminates, taking the background thread with
it.
When a thread terminates because of its parent process,
it stops dead, and no finally blocks are executed.
The same situation arises when a user terminates an unresponsive
application via the Windows Task Manager, or a process is killed programmatically
via Process.Kill.
(Sưu tầm) |