Introduction
This article will explain how to detect if a thread is in a problematic situation before calling Abort
.
Background
I wrote an article about the using
keyword explaining how it is not Abort()
safe, and presented a solution to avoid leaks if an Abort()
is called at the wrong time. But, many people argued that doing a code fully protected against Abort
is hard to maintain, slow, will not really work as .NET itself uses the "using
" keyword and, if one really wants to abort immediately, such a technique can make the program "unresponsive" and it will be a bad user experience. So...
Another Approach
What I really wanted was not a solution that changed every "using
" clause, instead I wanted that Abort()
will recognize the IDisposable
interface, not aborting (at least not for some time) until the IDisposable
constructor returns and is assigned to the local variable. I even asked Microsoft to change how Abort()
works, but they didn't consider my solution so important, as "Abort
" is uncommon. Well, I think .NET should never leak memory, even when abort happens. So I was still looking for a solution. The one I found is not the ultimate solution, but I consider it at least less problematic.
Before continuing, a look at CancellationTokens and other Abort() alternatives
You can ask me why I am thinking about Thread.Abort()
when Microsoft is investing in Tasks instead of Threads, which have the CancellationToken
s and do not risk causing the same memory leaks that Abort
does. Well, I don't see Abort
as a collaborative way to cancel threads, I see Abort()
as a "manager" resource to force a Thread to stop if it is not answering such a request. CancellationToken
s are one of the many ways to ask a thread to stop its current work. I think anyone who has used threads before has already used boolean flags to tell a Thread to stop. This is, of course, a right way to do it. But even IIS needs to cancel a Thread forcibly if it does not finish its work in time. For that, Abort()
is still necessary, but I think its actual risks make it very problematic to use.
Abort Risks
There is only one real risk with Thread.Abort()
. It can happen at any MSIL instruction. So, it can happen in the middle of the constructor of a File
object, just after the File
was constructed, but before it is set to a variable and, so, a normal new/try/finally
block will not protect your code from an Abort
.
Someone might say: OK, but then the object will be discarded during the next Garbage Collection, and that is partially true. If the Disposable
object has a destructor, its destructor will be called during the next Garbage Collection. But, it is possible that its constructor was stopped just after allocating a resource, and before setting it to an instance variable, so the destructor itself will never consider such a resource to be allocated, and will never deallocate it.
So, how can we solve that?
Thread.Suspend, StackTrace, and Thread.Resume
The Thread.Suspend
and Thread.Resume
methods are marked as [Obsolete], and may be excluded from future versions of .NET. I really hope Microsoft never does that, at least if Abort
is not corrected first.
In fact, all I do is suspend the target thread. If the thread is inside the constructor of a IDisposable
object, or inside a function that returns an IDisposable
object, I consider it can't be aborted.
If I can abort, I abort it while suspended, so I know that Abort
will happen exactly at the point I verified the thread. And finally, I resume the execution of the thread.
Advantages of this Approach
- It is guaranteed that an
Abort
will never happen while an IDisposable
object is being constructed, so it will not leak memory or other resources; - It does not require a change in every allocation of an
IDisposable
object as in the solution I presented in the other article; - It does not make the code
Abort
resistant, so you can still force an immediate Abort
if you need.
Disadvantages of this Approach
- If the
Abort
is being done by an external executable (like IIS), you can't make it use this approach, so you will still have all the risks of the common Abort
, or will need to protect your code from an Abort
that may happen at any time; - You can't choose where to put the
Abort()
; it will not see this: you are inside a constructor, so put the Abort
after it; it will only see: this is a constructor, so can't abort now; in fact, you may need to retry Abort
many times if the thread keeps creating IDisposable
objects (like in a loop); - This will also not guarantee that the "
using
" keyword or a common try/finally
block will work; if you just created an object but still haven't time to put it into a local variable, Abort()
will lose the reference; but now, it may be correctly reclaimed by the Garbage Collector, so calling a GC.Collect
can solve the problem; - But the worst,
Thread.Suspend()
can dead-lock; in this case, I have no work-around for it; I even tried to force a Thread.Resume
from another thread, but that didn't work; in my tests, this only happened when creating the thread, so waiting a little before suspending is OK, but I do not know the real reason for this to happen, or where else it can dead-lock.
Speed
In my tests, SafeAbort()
only gets a little slower than the real Abort()
. I even did one extra check in the code: if the thread-to-be-aborted is inside a catch
or finally
block, even if it is allocating an IDisposable
object, it can be aborted, as the Abort(
) is delayed in catch
/finally
blocks. This happens as soon as the catch
/finally
block(s) are finished, and is much better than retrying. But in my tests, using Abort()
leaks resources forever, while my solution rarely loses references, and even when it does, the next Garbage Collection solves the problem.
The Future
I don't think I will change this code very much. I really tried analysing the IL to try to discover if the code is about to store a result into a variable, to guarantee that every finally
block will work as expected, but I did not manage to get all the situations, as optimization can change the "expected" code a lot. Also, I finished with a code that almost never allowed to Abort
, which didn't help if the idea is to create a "fast" Abort
alternative which is less prone to memory leaks/corruptions. I really hope that a real solution is done by Microsoft, which could guarantee that Abort()
will never happen inside a using()
allocator (at least not for some seconds), improving code reliability and not requiring to use obsolete APIs like Thread.Suspend()
, and also without the risk of causing a dead-lock.
In fact, I don't consider this to be a very useable solution. I only think this should be considered by anyone creating a server program that may need to abort threads, as I really think this is safer than calling Thread.Abort
directly.
And Some Days Later...
You probably just saw that I didn't want to update the article. But, to be honest, I found a solution that guarantees using blocks to work. In fact, I tried that solution before, saw a problem in it but, after rethinking, I make it work.
The idea is simple. I am never sure where I can Abort inside a method that has a using
clause, because of the optimizations. But, I can discover if the method has at least one try
block. If it has, I consider such method not to be abortable, so I will be sure I will never abort between an allocation and a try
, but this can cause another problem: What if the user does a try
/finally
block and, after, an infinite loop?
Such infinite loop, if it is still calling another method, will be abortable inside such method. If it is really a simple infinite loop, it will only be aborted if I am doing a less restrictive Abort. That's why I added a SafeAbortMode
parameter in my Abort
. But, to be honest, if the user wants to create an inabortable problematic thread, the user can always put an infinite loop inside a finally block. Today, there is no way to force an abort in such situation, but I hope that's not a common situation.
And, to make it complete, I also added a Validating
event, in which you can do additional validations. After all, now I can guarantee that a using
block will never fail, but something simple like:
Begin();
try
{
SomeCode();
}
finally
{
End();
}
Can still suffer the consequences of an abort happening inside Begin()
or between the Begin()
return and the try
block. So, if you know which methods suffer from that consequence, and can't change them to use an IDisposable
structure, you can validate them. In the updated sample, I use that to avoid Aborts()
from happening in my "_Sum
" method.
The Code
The attached sample simply keeps creating and aborting threads, where those threads open and close files or lock and unlock objects. If you start to see a lot of consecutive "E"s, that means that the file can't be created. This could happen if you don't have access to write a file (and so, has nothing to do with the Abort
), or it means that even after a garbage collection, the FileStream
handle was not deallocated. If everything is fine, such a situation will only happen when using a normal Abort()
, as AbortIfSafe()
will never corrupt a FileStream
object.
If you can't download the file (unregistered users), here is the full code for the SafeAbort
class. I hope it is at least useful to understand the StackTrace
and StackFrame
classes.
using System;
using System.Reflection;
using System.Threading;
using System.Security.Permissions;
namespace Pfz.Threading
{
public static class SafeAbort
{
public static bool AbortIfSafe(Thread thread,
SafeAbortMode mode=SafeAbortMode.RunAllValidations, object stateInfo=null)
{
if (thread == null)
throw new ArgumentNullException("thread");
if (thread == Thread.CurrentThread)
thread.Abort(stateInfo);
switch (thread.ThreadState & ~(ThreadState.Suspended |
ThreadState.SuspendRequested))
{
case ThreadState.Running:
case ThreadState.Background:
case ThreadState.WaitSleepJoin:
break;
case ThreadState.Stopped:
case ThreadState.StopRequested:
case ThreadState.AbortRequested:
case ThreadState.Aborted:
return true;
default:
throw new ThreadStateException
("The thread is in an invalid state to be aborted.");
}
try
{
thread.Suspend();
}
catch(ThreadStateException)
{
switch (thread.ThreadState & ~(ThreadState.Suspended |
ThreadState.SuspendRequested))
{
case ThreadState.Aborted:
case ThreadState.Stopped:
return true;
}
throw;
}
while (thread.ThreadState == ThreadState.SuspendRequested)
Thread.Sleep(1);
if ((thread.ThreadState & (ThreadState.Stopped |
ThreadState.Aborted)) != ThreadState.Running)
return true;
try
{
var stack = new System.Diagnostics.StackTrace(thread, false);
var frames = stack.GetFrames();
if (frames == null)
return false;
bool? canAbort = null;
for (int i = frames.Length - 1; i >= 0; i--)
{
var frame = frames[i];
var method = frame.GetMethod();
if (method.IsConstructor)
{
ConstructorInfo constructorInfo = (ConstructorInfo)method;
if (typeof(IDisposable).IsAssignableFrom
(constructorInfo.DeclaringType))
{
canAbort = false;
break;
}
}
else
{
MethodInfo methodInfo = (MethodInfo)method;
if (typeof(IDisposable).IsAssignableFrom(methodInfo.ReturnType))
{
canAbort = false;
break;
}
}
var attributes = (HostProtectionAttribute[])method.
GetCustomAttributes(typeof(HostProtectionAttribute), false);
foreach (var attribute in attributes)
{
if (attribute.MayLeakOnAbort)
{
canAbort = false;
break;
}
}
attributes = (HostProtectionAttribute[])method.DeclaringType.
GetCustomAttributes(typeof(HostProtectionAttribute), false);
foreach (var attribute in attributes)
{
if (attribute.MayLeakOnAbort)
{
canAbort = false;
break;
}
}
attributes = (HostProtectionAttribute[])method.DeclaringType.
Assembly.GetCustomAttributes(typeof(HostProtectionAttribute),
false);
foreach (var attribute in attributes)
{
if (attribute.MayLeakOnAbort)
{
canAbort = false;
break;
}
}
var body = method.GetMethodBody();
if (body == null)
continue;
int offset = frame.GetILOffset();
foreach (var handler in body.ExceptionHandlingClauses)
{
int handlerOffset = handler.HandlerOffset;
int handlerEnd = handlerOffset + handler.HandlerLength;
if (offset >= handlerOffset && offset < handlerEnd)
{
canAbort = true;
break;
}
if (canAbort.GetValueOrDefault())
break;
}
}
if (canAbort == null)
{
if (mode == SafeAbortMode.AllowUsingsToFail)
canAbort = true;
else
{
var frame = frames[0];
var method = frame.GetMethod();
var body = method.GetMethodBody();
if (body != null)
{
var handlingClauses = body.ExceptionHandlingClauses;
if (handlingClauses.Count == 0)
{
canAbort = true;
if (mode == SafeAbortMode.RunAllValidations)
{
var handler = Validating;
if (handler != null)
{
SafeAbortEventArgs args = new SafeAbortEventArgs
(thread, stack, frames);
handler(null, args);
canAbort = args.CanAbort;
}
}
}
}
}
}
if (canAbort.GetValueOrDefault())
{
try
{
thread.Abort(stateInfo);
}
catch
{
}
return true;
}
return false;
}
finally
{
thread.Resume();
}
}
public static bool Abort(Thread thread, int triesWithAllValidations,
int triesIgnoringUserValidations, int triesAllowingUsingsToFail,
bool finalizeWithNormalAbort = false, object stateInfo = null)
{
if (thread == null)
throw new ArgumentNullException("thread");
for (int i = 0; i < triesWithAllValidations; i++)
{
if (AbortIfSafe(thread, SafeAbortMode.RunAllValidations, stateInfo))
return true;
Thread.Sleep(1);
}
for (int i = 0; i < triesIgnoringUserValidations; i++)
{
if (AbortIfSafe(thread, SafeAbortMode.IgnoreUserValidations, stateInfo))
return true;
Thread.Sleep(1);
}
for (int i = 0; i < triesAllowingUsingsToFail; i++)
{
if (AbortIfSafe(thread, SafeAbortMode.AllowUsingsToFail, stateInfo))
return true;
Thread.Sleep(1);
}
if (finalizeWithNormalAbort)
{
thread.Abort(stateInfo);
return true;
}
return false;
}
public static event EventHandler<SafeAbortEventArgs> Validating;
}
}
History
- 9th February, 2011: Initial post
- 15th February, 2011: Added
AbortSafeMode
, which can now allow for user-validations, can guarantee that "using
" will always be called, or can work as in the old version, only guaranteeing that IDisposable
objects are never corrupted by an Abort