65.9K
CodeProject 正在变化。 阅读更多。
Home

AbortIfSafe

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (30投票s)

2011年2月9日

CPOL

9分钟阅读

viewsIcon

55601

downloadIcon

217

在调用 Abort 之前检测线程是否处于问题情况的方法

引言

本文将解释如何在调用 Abort 之前检测线程是否处于问题情况。

背景

我写了一篇关于 `using` 关键字的文章,解释了它为何不 Abort() 安全,并提出了一个解决方案,以避免在 Abort() 被错误调用时发生内存泄漏。但是,许多人认为,编写一个完全不受 Abort 影响的代码很难维护、速度慢,而且实际上并不能真正起作用,因为 .NET 本身就使用了“using”关键字。而且,如果真的要立即中止,这种技术可能会使程序“无响应”,导致糟糕的用户体验。所以……

另一种方法

我真正想要的是一个不改变每个“using”子句的解决方案,而是希望 Abort() 能够识别 IDisposable 接口,而不是(至少在一段时间内)中止,直到 IDisposable 构造函数返回并赋值给局部变量。我甚至向微软请求更改 Abort() 的工作方式,但他们没有认为我的解决方案很重要,因为“Abort”并不常用。好吧,我认为 .NET 即使在中止时也不应该泄漏内存。所以我一直在寻找解决方案。我找到的这个并非终极解决方案,但我认为它至少问题更少。

在继续之前,让我们看一下 CancellationTokens 和其他 Abort() 替代方案

你可能会问,既然微软正在投入 Task 而非 Thread,而 Task 拥有 CancellationToken 并且不会像 Abort 那样导致内存泄漏,为什么我还要考虑 Thread.Abort()?嗯,我不认为 Abort 是一种协作式的线程取消方式,我将 Abort() 视为一个“管理器”资源,用于强制线程停止,如果它没有响应这种请求的话。CancellationToken 是请求线程停止当前工作的众多方式之一。我认为任何使用过线程的人都曾使用布尔标志来告诉线程停止。这当然是正确的方式。但是,即使是 IIS 也需要强制取消一个线程,如果它没有及时完成工作的话。为此,Abort() 仍然是必要的,但我认为它的实际风险使其使用起来 **非常** 成问题。

Abort 的风险

Thread.Abort() 只有一个真正的风险。它可能发生在任何 MSIL 指令处。因此,它可能发生在 File 对象构造函数的中间,就在 File 被构造之后,但在赋值给变量之前,所以,一个普通的 new/try/finally 块将无法保护你的代码免受 Abort 的影响。

有人可能会说:好吧,那对象会在下次垃圾回收时被丢弃,这在一定程度上是正确的。如果 Disposable 对象有析构函数,它的析构函数将在下次垃圾回收时被调用。但是,它的构造函数可能恰好在分配资源后、赋值给实例变量之前就停止了,所以析构函数本身永远不会认为该资源已被分配,也永远不会释放它。

那么,我们该如何解决这个问题呢?

Thread.Suspend、StackTrace 和 Thread.Resume

Thread.SuspendThread.Resume 方法被标记为 [Obsolete],并且可能在 .NET 的未来版本中被移除。我真的希望微软永远不要这样做,至少在 Abort 被修正之前。

实际上,我所做的就是挂起目标线程。如果线程正在执行 IDisposable 对象的构造函数,或者在返回 IDisposable 对象的函数内,我会认为它不能被中止。

如果我可以中止,我会在线程挂起时中止它,这样我就知道 Abort 将发生在精确我验证线程的点。最后,我将恢复线程的执行。

此方法的优点

  • 保证 Abort 绝不会发生在 IDisposable 对象构造过程中,因此不会泄漏内存或其他资源;
  • 不需要像我在另一篇文章中提出的解决方案那样,更改每个 IDisposable 对象的分配;
  • 它不会使代码对 Abort 产生抵抗力,因此如果你需要,仍然可以强制立即 Abort

此方法的缺点

  • 如果 Abort 是由外部可执行文件(如 IIS)执行的,你无法让它使用此方法,因此你仍然会面临普通 Abort 的所有风险,或者需要保护你的代码免受随时可能发生的 Abort 的影响;
  • 你无法选择将 Abort() 放在哪里;它看不到这一点:你在构造函数中,所以将 Abort 放在构造函数之后;它只会看到:这是一个构造函数,现在不能中止;事实上,如果线程不断创建 IDisposable 对象(例如在循环中),你可能需要多次重试 Abort
  • 这也不能保证“using”关键字或普通的 try/finally 块能够正常工作;如果你刚刚创建了一个对象但还没有时间将其放入局部变量,Abort() 将会丢失引用;但是,现在它可能会被垃圾回收器正确回收,所以调用 GC.Collect 可以解决问题;
  • 但最糟糕的是,Thread.Suspend() 可能会导致死锁;在这种情况下,我没有任何解决方法;我甚至尝试从另一个线程强制执行 Thread.Resume,但没有成功;在我的测试中,这种情况只发生在创建线程时,所以在挂起之前等待一段时间是可以的,但我不知道这种情况发生的真正原因,或者它还可能在何处死锁。

速度

在我的测试中,SafeAbort() 仅比实际的 Abort() 慢一点。我甚至在代码中多做了一个检查:如果被中止的线程位于 catchfinally 块内,即使它正在分配一个 IDisposable 对象,也可以被中止,因为 Abort()catch/finally 块中被延迟。这种情况会在 catch/finally 块执行完毕后立即发生,这比重试要好得多。但是在我的测试中,使用 Abort() 会永远泄漏资源,而我的解决方案很少丢失引用,即使丢失了,下一次垃圾回收也会解决问题。

未来

我不认为我会对这段代码做太多改动。我确实尝试分析 IL 来发现代码是否即将把结果存储到变量中,以保证每个 finally 块都能按预期工作,但我未能涵盖所有情况,因为优化可能会很大程度上改变“预期”的代码。此外,我最终得到了一段几乎从不允许 Abort 的代码,这无助于创建“快速”的 Abort 替代方案,而且不易发生内存泄漏/损坏。我真心希望微软能提供一个真正的解决方案,保证 Abort() 永远不会发生在 using() 分配器内部(至少在几秒钟内不会),从而提高代码的可靠性,并且不需要使用像 Thread.Suspend() 这样的过时 API,也不会有导致死锁的风险。

事实上,我不认为这是一个非常可用的解决方案。我认为只有创建服务器程序并且可能需要中止线程的人才应该考虑它,因为我真的认为这比直接调用 Thread.Abort 更安全。

几天后...

你可能刚刚注意到我不想更新这篇文章。但说实话,我找到了一个可以保证 `using` 块正常工作的解决方案。事实上,我之前尝试过那个解决方案,发现了一个问题,但在重新思考后,我让它奏效了。

这个想法很简单。由于优化,我无法确定在具有 `using` 子句的方法的哪里可以 Abort。但是,我可以发现该方法至少有一个 `try` 块。如果有,我会认为该方法不可中止,因此我将确保我永远不会在分配和 `try` 之间中止。但这可能会导致另一个问题:如果用户执行了一个 `try`/`finally` 块,之后又是一个无限循环怎么办?

这种无限循环,如果还在调用另一个方法,那么将在该方法内部被中止。如果它 **确实** 是一个简单的无限循环,那么只有在我执行一个限制性较小的 Abort 时才会中止。这就是为什么我在我的 Abort 中添加了一个 SafeAbortMode 参数。但说实话,如果用户想创建一个不可中止的麻烦线程,用户总可以在 `finally` 块中放入一个无限循环。目前,在这种情况下无法强制中止,但我希望这种情况不常见。

而且,为了使其完整,我还添加了一个 Validating 事件,你可以在其中执行额外的验证。毕竟,现在我可以保证 `using` 块永远不会失败,但一些简单的操作,如

Begin();
try
{
    SomeCode();
}
finally
{
    End();
}

仍然可能遭受在 Begin() 内部或 Begin() 返回和 `try` 块之间发生的 abort 的后果。因此,如果你知道哪些方法会遭受这种后果,并且无法将它们更改为使用 IDisposable 结构,你可以验证它们。在更新的示例中,我使用它来避免在我的“_Sum”方法中发生 Aborts()

代码

附带的示例只是不断地创建和中止线程,这些线程会打开和关闭文件或锁定和解锁对象。如果你开始看到大量的连续“E”,这意味着文件无法创建。这可能是因为你没有写入文件的权限(因此与 Abort 无关),或者这意味着即使在垃圾回收之后,FileStream 句柄也没有被释放。如果一切正常,这种情况只会在使用普通 Abort() 时发生,因为 AbortIfSafe() 永远不会损坏 FileStream 对象。

如果你无法下载文件(未注册用户),以下是 SafeAbort 类的完整代码。我希望它至少有助于理解 StackTraceStackFrame 类。

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;
    }
}

历史

  • 2011 年 2 月 9 日:初始帖子
  • 2011 年 2 月 15 日:添加了 AbortSafeMode,该模式现在允许用户验证,可以保证“using”始终会被调用,或者可以像旧版本那样工作,只保证 IDisposable 对象不会被 Abort 损坏。
© . All rights reserved.