是否可以解释PrepareConstrainedRegions和Thread.Abort的这种意外行为?

Bri*_*eon 5 .net c# multithreading cer threadabortexception

今晚我与约束执行区一起玩耍,以更好地完善我对更好细节的理解。我以前曾经使用过它们,但是在那种情况下,我主要严格遵守既定模式。无论如何,我注意到了一些我无法完全解释的奇特之处。

考虑下面的代码。注意,我的目标是.NET 4.5,并在没有附加调试器的情况下使用Release版本对其进行了测试。

public class Program
{
    public static void Main(string[] args)
    {
        bool toggle = false;
        bool didfinally = false;
        var thread = new Thread(
            () =>
            {
                Console.WriteLine("running");
                RuntimeHelpers.PrepareConstrainedRegions();
                try
                {
                    while (true) 
                    {
                      toggle = !toggle;
                    }
                }
                finally
                {
                    didfinally = true;
                }
            });
        thread.Start();
        Console.WriteLine("sleeping");
        Thread.Sleep(1000);
        Console.WriteLine("aborting");
        thread.Abort();
        Console.WriteLine("aborted");
        thread.Join();
        Console.WriteLine("joined");
        Console.WriteLine("didfinally=" + didfinally);
        Console.Read();
    }
}
Run Code Online (Sandbox Code Playgroud)

您认为该程序的输出是什么?

  1. didfinally =真
  2. didfinally =假

在猜测之前,请阅读文档。我在下面包括相关部分。

约束执行区(CER)是创作可靠的托管代码的机制的一部分。CER定义了一个区域,在该区域中,公共语言运行库(CLR)受到约束,无法抛出带外异常,这将阻止该区域中的代码整体执行。在该区域内,用户代码无法执行代码,从而导致抛出带外异常。PrepareConstrainedRegions方法必须立即在try块之前,并将catch块,fault块和Fault块标记为受约束的执行区域。一旦标记为受约束区域,代码就只能调用具有强可靠性合同的其他代码,并且除非代码准备好处理失败,否则代码不应分配或对未准备好的方法或不可靠的方法进行虚拟调用。对于在CER中执行的代码,CLR延迟线程中止。

可靠性try / catch / finally是一种异常处理机制,具有与非托管版本相同的可预测性保证。捕获/最终阻止是CER。块中的方法需要预先准备,并且必须不间断。

我现在特别关心的是防止线程中止。有两种:一种是您的常规选择Thread.Abort,另一种是CLR主机可以在您身上经历整个中世纪并强制中止。finally块已经受到Thread.Abort一定程度的保护。然后,如果您将该finally块声明为CER,那么您还可以获得来自CLR主机中止的附加保护……至少我认为这是理论。

因此,根据我的想法,我知道我猜到了第一。它应该打印didfinally = True。将ThreadAbortException被注入,而代码仍然是在try块,然后CLR允许finally以正如所预料的,即使没有CER权运行阻滞?

好吧,这不是我得到的结果。我得到了完全出乎意料的结果。#1或#2都没有发生在我身上。相反,我的程序挂在Thread.Abort。这是我观察到的。

  • PrepareConstrainedRegions延迟线程的存在会在try块内部中止。
  • 缺少PrepareConstrainedRegions允许他们成try块。

那么百万美元的问题是为什么呢?该文档没有提到我可以看到的任何地方的这种行为。实际上,我正在阅读的大多数内容实际上都建议您将重要的不间断代码放在该finally块中,专门用于防止线程中止。

也许,除了该块之外,还PrepareConstrainedRegions延迟了try块中的正常中止finally。但是,CLR主机中止只会在finallyCER块中延迟吗?谁能对此提供更多的清晰度?

The*_*ide 2

[续评论]

我将把我的答案分为两部分:CER 和处理 ThreadAbortException。

我不认为 CER 首先是为了帮助解决线程中止问题;这些不是您要找的机器人。我也可能误解了问题的陈述,这些东西往往会变得相当沉重,但我发现文档中的关键短语(不可否认,其中之一实际上与我提到的不同部分)是:

The code cannot cause an out-of-band exception

user code creates non-interruptible regions with a reliable try/catch/finally that *contains an empty try/catch block* preceded by a PrepareConstrainedRegions method call

尽管没有直接在受约束的代码中受到启发,但线程中止是一个带外异常。受约束区域仅保证,一旦finally执行,只要它遵守它所承诺的约束,它就不会被托管运行时操作中断,否则不会中断非托管finally块。线程中止会中断非托管代码,就像它们中断托管代码一样,但如果没有受约束的区域,则有一些保证,并且对于您可能正在寻找的行为可能还有不同的推荐模式。我怀疑这主要是作为垃圾收集线程挂起的屏障(如果我不得不猜测的话,可能是通过在该区域的持续时间内将线程从抢占式垃圾收集模式切换出来)。我可以想象将其与弱引用、等待句柄和其他低级管理例程结合使用。

至于意外的行为,我的想法是,你没有履行你通过声明受限区域所承诺的合同,因此结果没有记录,应该被认为是不可预测的。线程中止在尝试中被推迟确实看起来很奇怪,但我相信这是意外使用的副作用,这仅值得进一步探索以对运行时进行学术理解(一类易失的知识,因为无法保证该行为,未来的更新可能会改变该行为)。

现在,我不确定以意想不到的方式使用上述副作用的程度有多大,但如果我们退出使用原力影响我们的控制身体的背景并让事情按照正常方式运行,我们确实得到了一些保证:

  • 在某些情况下,Thread.ResetAbort 可以防止线程中止
  • ThreadAbortException 可以被捕获;整个 catch 块将运行,并且如果未重置中止,则退出 catch 块时将自动重新抛出 ThreadAbortException。
  • 当 ThreadAbortException 展开调用堆栈时,所有的 finally 块都保证运行。

因此,这里有一个技术示例,用于需要中止弹性的情况。我在一个示例中混合了多种技术,这些技术不需要同时使用(通常您不会),只是为了根据您的需求提供选项样本。

bool shouldRun = true;
object someDataForAnalysis = null;

try {

    while (shouldRun) {
begin:
        int step = 0;
        try {

            Interlocked.Increment(ref step);
step1:
            someDataForAnalysis = null;
            Console.WriteLine("test");

            Interlocked.Increment(ref step);
step2:

            // this does not *guarantee* that a ThreadAbortException will not be thrown,
            // but it at least provides a hint to the host, which may defer abortion or
            // terminate the AppDomain instead of just the thread (or whatever else it wants)
            Thread.BeginCriticalRegion();
            try {

                // allocate unmanaged memory
                // call unmanaged function on memory
                // collect results
                someDataForAnalysis = new object();
            } finally {
                // deallocate unmanaged memory
                Thread.EndCriticalRegion();
            }

            Interlocked.Increment(ref step);
step3:
            // perform analysis
            Console.WriteLine(someDataForAnalysis.ToString());
        } catch (ThreadAbortException) {
            // not as easy to do correctly; a little bit messy; use of the cursed GOTO (AAAHHHHHHH!!!! ;p)
            Thread.ResetAbort();

            // this is optional, but generally you should prefer to exit the thread cleanly after finishing
            // the work that was essential to avoid interuption. The code trying to abort this thread may be
            // trying to join it, awaiting its completion, which will block forever if this thread doesn't exit
            shouldRun = false;

            switch (step) {
                case 1:
                    goto step1;
                    break;
                case 2:
                    goto step2;
                    break;
                case 3:
                    goto step3;
                    break;
                default:
                    goto begin;
                    break;
            }
        }
    }

} catch (ThreadAbortException ex) {
    // preferable approach when operations are repeatable, although to some extent, if the
    // operations aren't volatile, you should not forcibly continue indefinite execution
    // on a thread requested to be aborted; generally this approach should only be used for
    // necessarily atomic operations.
    Thread.ResetAbort();
    goto begin;
}
Run Code Online (Sandbox Code Playgroud)

我不是 CER 方面的专家,所以如果我有误解,请任何人告诉我。我希望这有帮助 :)