Thread in C# - Part 19: Aborting Threads  
 

(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)


 
 

 
     
 
Công nghệ khác:


Thread in C# - Part 18: Suspend and ResumeThread in C# - Part 17: Wait and Pulse (2)
Thread in C# - Part 17: Wait and Pulse (1)Thread in C# - Part 16: Non-Blocking Synchronization
Thread in C# - Part 15: Local StorageThread in C# - Part 14: Timers
  Xem tiếp    
 
Lịch khai giảng của hệ thống
 
Ngày
Giờ
T.Tâm
TP Hồ Chí Minh
Hà Nội
 
   
New ADSE - Nhấn vào để xem chi tiết
Mừng Sinh Nhật Lần Thứ 20 FPT-APTECH
Nhấn vào để xem chi tiết
Bảng Vàng Thành Tích Sinh Viên FPT APTECH - Nhấn vào để xem chi tiết
Cập nhật công nghệ miễn phí cho tất cả cựu sinh viên APTECH toàn quốc
Tiết Thực Vì Cộng Đồng
Hội Thảo CNTT
Những khoảnh khắc không phai của Thầy Trò FPT-APTECH Ngày 20-11