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

5
今晚我在玩受限执行区域,以更好地了解其细节。之前我有时使用它们,但那些情况下我大多严格遵循已建立的模式。无论如何,我注意到了一些奇怪的东西,我不能很好地解释。
考虑以下代码。请注意,我针对.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();
    }
}

你认为这个程序的输出会是什么?

  1. didfinally=True
  2. didfinally=False

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

受限执行区域(CER)是编写可靠托管代码机制的一部分。 CER定义了一个区域,在该区域中,公共语言运行时(CLR)被限制抛出超出范围的异常,该异常将阻止代码在该区域内完全执行。在该区域内,用户代码不能执行会导致超出范围异常的代码。 PrepareConstrainedRegions方法必须紧接在try块之前,并将catch、finally和fault块标记为受限执行区域。一旦标记为受限区域,代码只能调用具有强可靠性契约的其他代码,并且除非代码准备好处理失败,否则代码不应分配或进行虚拟调用到未准备或不可靠的方法。 CLR延迟执行在CER中执行的代码的线程中止操作。

可靠性的try/catch/finally是一种异常处理机制,具有与非托管版本相同级别的可预测性保证。catch/finally块是CER。块中的方法需要提前准备并且必须是不可中断的。
我现在特别关注防止线程中止。有两种情况:通过Thread.Abort进行的普通类型和CLR主机可以对您进行强制中止的类型。finally块已经在某种程度上受到了Thread.Abort的保护。然后,如果您将该finally块声明为CER,则还会获得来自CLR主机中止的额外保护...至少我认为这就是理论。
所以基于我所知道的,我猜#1。它应该打印didfinally=True。ThreadAbortException在代码仍在try块中时被注入,然后CLR允许finally块按预期运行,即使没有CER,对吗?

好的,这不是我得到的结果。我得到了一个完全意外的结果。对我来说,既没有发生#1也没有发生#2。相反,我的程序在Thread.Abort处挂起。以下是我观察到的情况。

  • PrepareConstrainedRegions的存在会延迟try块内的线程中止。
  • PrepareConstrainedRegions的缺失允许在try块中中止。

所以百万美元的问题是为什么?文档中没有提到这种行为。事实上,我正在阅读的大部分内容实际上都建议将关键的不间断代码放在finally块中,特别是为了防止线程中止。

也许,PrepareConstrainedRegions除了在finally块中之外,还会延迟try块中的正常中止。但是CLR主机中止只能在CER的finally块中延迟吗?有人能提供更多关于此问题的澄清吗?

3个回答

2

[从评论继续]

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

我不相信CER本来就是为了帮助处理线程中止;这些不是你要找的东西。我可能也误解了问题陈述,这些问题往往变得非常复杂,但我在文档中找到的关键短语(其中一个确实在我提到的不同部分中)是:

代码不能导致带外异常

用户代码使用可靠的try/catch/finally创建不可中断区域,该区域包含一个PrepareConstrainedRegions方法调用之前的空try/catch块

尽管受限制的代码没有直接启发线程中止,但线程中止是一种带外异常。受限制的区域仅保证,一旦finally正在执行,只要它遵守了它所承诺的约束,它不会被中断以进行管理运行时操作,否则不会中断未经管理的finally块。线程中止中断未经管理的代码,就像中断经过管理的代码一样,但是如果没有受限制的区域,则可能有一些保证和不同的推荐模式,以获得您可能正在寻找的行为。我认为,这主要是用于防止垃圾回收的线程暂停(可能通过在区域持续时间内将线程切换出预先计划的垃圾回收模式来实现)。我可以想象与弱引用、等待句柄和其他低级管理例程结合使用。

至于意外行为,我的想法是你没有遵守声明受限制区域时承诺的契约,因此结果未经记录,应视为不可预测。Thread Abort在try中被延迟似乎很奇怪,但我认为这是意外使用的副作用,只有在学术理解运行时的情况下才值得进一步探索(这是一类知识,因为没有保证行为的未来更新可能会更改这种行为而变得不稳定)。

现在,我不确定在意外使用上述方法时副作用的范围是什么,但如果我们退出使用力量影响我们的控制身体的环境,并让事情按照通常的方式运行,我们确实会得到一些保证:

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

因此,这是一个旨在在需要中止弹性的情况下使用的技术示例。我在单个示例中混合了多种不必同时使用的技术(通常您不会这样做),只是为了让您根据需要选择选项。

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

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


1
你提出了一些好观点。我认为我的困惑大多源于文档暗示try块不是CER的一部分。那么,为什么我们应该期望带外异常在try块执行时被延迟呢?通常,在PrepareConstrainedRegions之后立即看到空的try块。然而,我喜欢你关于这种情况下“未定义行为”的观点。我肯定可以接受这个理由。我打算接受这个答案。 - Brian Gideon
是的,我曾经两次认为我理解了发生的事情,直到我意识到整个情况超出了所描述的上下文范围,哈哈。 - TheXenocide

2

我认为至少有一个假设可以解释这种情况。如果将while循环更改为将线程放入可警告状态,即使设置了CER,也会注入ThreadAbortException

RuntimeHelpers.PrepareConstrainedRegions();
try
{
   // Standard abort injections are delayed here.

   Thread.Sleep(1000); // ThreadAbortException can be injected here.

   // Standard abort injections are delayed here.
}
finally
{
    // CER code goes here.
    // Most abort injections are delayed including those forced by the CLR host.
}

所以,PrepareConstrainedRegions 将会使得由 Thread.Aborttry 块内发出的异常降级,以便更像 Thread.Interrupt。很容易理解这将使得 try 块内的代码更加安全。异常会被延迟直到一个点,此时数据结构更可能处于一致状态。当然,这假设开发人员不会故意(或无意中)将线程置于可警告状态,而正好在更新关键数据结构时。
因此,基本上 PrepareConstrainedRegions 具有进一步约束在 try 内注入异常的未记录功能。由于这个功能没有记录,开发者最好避免依赖这一假设,不要将关键代码放在 CER 结构的 try 块中。按照文档,只有 catchfinallyfault(不适用于 C#)块被正式定义为 CER 的作用域。

我认为MSDN文档中关于CER的“约束”部分已经足够解释了哪些内容是被处理的,哪些不是:http://msdn.microsoft.com/en-us/library/ms228973.aspx - TheXenocide
@TheXenocide:如果我理解正确,它告诉你(程序员)在CER中不能做什么。我不认为它告诉你try块的行为(根据文档,它并不是CER的技术部分)。我有遗漏什么吗? - Brian Gideon
我的回答开始变得冗长而复杂,所以我只是添加了一个真正的答案。 - TheXenocide

1
你的意外行为是由于你的代码具有最大可靠性。
定义以下方法:
private static bool SwitchToggle(bool toggle) => !toggle;

[ReliabilityContract(Consistency.WillNotCorruptState,Cer.Success)]
private static bool SafeSwitchToggle(bool toggle) => !toggle;

使用它们代替 while 循环的主体。当调用 SwitchToggle 时,循环变得可中止;而调用 SafeSwitchToggle 时则不再可中止。

如果在 try 块内添加任何其他方法,但没有 Consistency.WillNotCorruptState 或 Consistency.MayCorruptInstance,则情况也是如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接