Click here to Skip to main content
15,868,340 members
Articles / Programming Languages / C#

AbortIfSafe

Rate me:
Please Sign up or sign in to vote.
4.83/5 (30 votes)
15 Feb 2011CPOL9 min read 52.4K   206   48   23
How to detect if a thread is in a problematic situation before calling Abort

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 CancellationTokens 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. CancellationTokens 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:

C#
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.

C#
using System;
using System.Reflection;
using System.Threading;
using System.Security.Permissions;

namespace Pfz.Threading
{
    /// <summary>
    /// Class that allows thread-aborts to be done in a relatively safe manner.
    /// </summary>
    public static class SafeAbort
    {
        /// <summary>
        /// Aborts a thread only if it is safe to do so, 
        /// taking the abort mode into account (which may
        /// range from only guaranteeing that IDisposable 
        /// objects will be fully constructed, up to 
        /// guaranteeing all "using" blocks to work and even doing some user validations).
        /// Returns if the Thread.Abort() was called or not.
        /// </summary>
        public static bool AbortIfSafe(Thread thread, 
	SafeAbortMode mode=SafeAbortMode.RunAllValidations, object stateInfo=null)
        {
            if (thread == null)
                throw new ArgumentNullException("thread");

            // If for some reason we are trying to abort our actual thread, 
	   // we simple call Thread.Abort() directly.
            if (thread == Thread.CurrentThread)
                thread.Abort(stateInfo);

            // We check the state of the thread, ignoring if the thread 
            // also has Suspended or SuspendRequested in its state.
            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:
                        // The thread terminated just when we are trying to Abort it. 
	               // So, we will return true to tell that the "Abort" succeeded.
                        return true;
                }

                // we couldn't discover why Suspend threw an exception, so we rethrow it.
                throw;
            }

            // We asked the thread to suspend, but the thread may take 
            // some time to be really suspended, so we will wait until 
            // it is no more in "SuspendRequested" state.
            while (thread.ThreadState == ThreadState.SuspendRequested)
                Thread.Sleep(1);

            // The Thread ended just when we asked it to suspend. 
            // So, for us, the Abort succeeded.
            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 we try to Abort the thread when it is starting (really soon), 
                // it will not have any frames. Calling an abort here caused 
                // some dead-locks for me,
                // so I consider that a Thread with no frames is a thread 
                // that can't be aborted.
                if (frames == null)
                    return false;

                bool? canAbort = null;
                // In the for block, we start from the oldest frame to the newest one.
                // In fact, we check this: If the method returns IDisposable, 
                // then we can't abort. If this is not the case, 
                // then if we are inside a catch or finally
                // block, we can abort, as such blocks delay the normal abort and, 
                // so, even if an internal call is inside a constructor of an 
                // IDisposable, we will not cause any problem calling abort. 
                // That's the reason to start with the oldest frame to the newest one.
                // And finally, if we are not in a problematic frame or in a 
                // guaranteed frame, we check if the method has try blocks. 
                // If it has, we consider we can't abort. Note that if you do a 
                // try/catch and then an infinite loop, this check will consider 
                // the method to be inabortable.
                for (int i = frames.Length - 1; i >= 0; i--)
                {
                    var frame = frames[i];
                    var method = frame.GetMethod();

                    // if we are inside a constructor of an IDisposable object 
                    // or inside a function that returns one, we can't abort.
                    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;
                        }
                    }

                    // Checks if the method, its class or its assembly 
                    // has HostProtectionAttributes with MayLeakOnAbort.
                    // If that's the case, then we can't abort.
                    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;

                    // if we were inside a finally or catch, we can abort, 
                    // as the normal Thread.Abort() will be naturally delayed.
                    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
                    {
                        // we are inside an unsure situation. 
                        // So, we will try to check the method.
                        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;

                                // Ok, by our tests we can abort. 
                                // But, if the mode is RunAllValidations and there 
                                // are user-validations, we must run them.
                                if (mode == SafeAbortMode.RunAllValidations)
                                {
                                    var handler = Validating;
                                    if (handler != null)
                                    {
                                        SafeAbortEventArgs args = new SafeAbortEventArgs
							(thread, stack, frames);
                                        handler(null, args);

                                        // The args by default has its 
                                        // CanAbort set to true. But, if any handler 
                                        // changed it, we will not be able to abort.
                                        canAbort = args.CanAbort;
                                    }
                                }
                            }
                        }
                    }
                }

                if (canAbort.GetValueOrDefault())
                {
                    try
                    {
                        // We need to call abort while the thread is suspended, 
                        // that works, but causes an exception, so we ignore it.
                        thread.Abort(stateInfo);
                    }
                    catch
                    {
                    }

                    return true;
                }

                return false;
            }
            finally
            {
                thread.Resume();
            }
        }

        /// <summary>
        /// Aborts a thread, trying to use the safest abort mode, until the unsafest one.
        /// The number of retries is also the expected number of milliseconds 
        /// trying to abort.
        /// </summary>
        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;
        }

        /// <summary>
        /// Event invoked by AbortIfSafe if user validations are valid 
        /// and when it is unsure if the thread
        /// is in a safe situation or not.
        /// </summary>
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions

 
GeneralMy vote of 5 Pin
Matthias Muenzner13-Jul-12 10:14
Matthias Muenzner13-Jul-12 10:14 
GeneralRe: My vote of 5 Pin
Paulo Zemek13-Jul-12 14:39
mvaPaulo Zemek13-Jul-12 14:39 
GeneralGood article Pin
BillW338-Mar-11 4:08
professionalBillW338-Mar-11 4:08 
GeneralMy vote of 4 Pin
jfriedman16-Feb-11 5:15
jfriedman16-Feb-11 5:15 
GeneralPotential cause of the dead lock Pin
Daniel Grunwald16-Feb-11 3:36
Daniel Grunwald16-Feb-11 3:36 
GeneralRe: Potential cause of the dead lock Pin
Paulo Zemek16-Feb-11 4:19
mvaPaulo Zemek16-Feb-11 4:19 
GeneralRe: Potential cause of the dead lock Pin
Daniel Grunwald16-Feb-11 4:41
Daniel Grunwald16-Feb-11 4:41 
GeneralRe: Potential cause of the dead lock Pin
Paulo Zemek16-Feb-11 5:13
mvaPaulo Zemek16-Feb-11 5:13 
GeneralRe: Potential cause of the dead lock [modified] Pin
Paulo Zemek16-Feb-11 7:12
mvaPaulo Zemek16-Feb-11 7:12 
General[My vote of 2] Calling Abort() is a bad practice in any case Pin
_groo_15-Feb-11 21:11
_groo_15-Feb-11 21:11 
GeneralRe: [My vote of 2] Calling Abort() is a bad practice in any case [modified] Pin
Paulo Zemek16-Feb-11 0:28
mvaPaulo Zemek16-Feb-11 0:28 
GeneralRe: [My vote of 2] Calling Abort() is a bad practice in any case Pin
_groo_16-Feb-11 7:58
_groo_16-Feb-11 7:58 
GeneralRe: [My vote of 2] Calling Abort() is a bad practice in any case Pin
Paulo Zemek16-Feb-11 8:08
mvaPaulo Zemek16-Feb-11 8:08 
GeneralRe: [My vote of 2] Calling Abort() is a bad practice in any case Pin
_groo_16-Feb-11 9:00
_groo_16-Feb-11 9:00 
GeneralRe: [My vote of 2] Calling Abort() is a bad practice in any case Pin
Paulo Zemek16-Feb-11 9:03
mvaPaulo Zemek16-Feb-11 9:03 
GeneralGood stuff Pin
Pete O'Hanlon11-Feb-11 12:05
subeditorPete O'Hanlon11-Feb-11 12:05 
I like to see authors exploring the outer reaches of the languages they use. Have a 5 from me.

I'm not a stalker, I just know things. Oh by the way, you're out of milk.

Forgive your enemies - it messes with their heads


My blog | My articles | MoXAML PowerToys | Onyx


GeneralMy vote of 5 Pin
BillaBong11-Feb-11 5:33
BillaBong11-Feb-11 5:33 
GeneralRe: My vote of 5 Pin
Paulo Zemek11-Feb-11 6:58
mvaPaulo Zemek11-Feb-11 6:58 
GeneralMy vote of 5 Pin
Marcelo Ricardo de Oliveira10-Feb-11 1:39
mvaMarcelo Ricardo de Oliveira10-Feb-11 1:39 
GeneralNeato Pin
Sacha Barber9-Feb-11 21:37
Sacha Barber9-Feb-11 21:37 
GeneralMy vote of 5 Pin
Gary Noble9-Feb-11 20:30
Gary Noble9-Feb-11 20:30 
GeneralMy vote of 5 Pin
JF20159-Feb-11 18:15
JF20159-Feb-11 18:15 
GeneralMy vote of 5 Pin
Uwe Keim9-Feb-11 17:49
sitebuilderUwe Keim9-Feb-11 17:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.