5

今晚我在玩Constrained Execution Regions,以更好地完善我对更精细细节的理解。我以前偶尔使用过它们,但在那些情况下,我大多严格遵守既定模式。无论如何,我注意到一些我无法完全解释的特殊情况。

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

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=真
  2. didfinally=假

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

受约束的执行区 (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延迟一个块中的正常中止。但是 CLR 主机中止仅在CER 块中延迟?谁能提供更清楚的说明?tryfinallyfinally

4

3 回答 3

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 Abort 在尝试中被推迟似乎确实很奇怪,但我认为这是意外使用的副作用,这仅值得进一步探索以对运行时进行学术理解(一类易变的知识,因为不能保证未来的更新可能会改变这种行为)。

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

  • 在某些情况下,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 方面的专家,所以如果我误解了,请告诉我。我希望这有帮助 :)

于 2015-01-16T20:04:55.977 回答
2

我想我至少对正在发生的事情有一个理论。如果while更改循环以使线程进入警报状态,那么ThreadAbortException即使使用 CER 设置也会注入。

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.AborttryThread.Interrupttry

因此,基本上PrepareConstrainedRegions具有添加的未记录功能,即在try. 由于此功能未记录在案,因此开发人员应谨慎避免依赖此假设,不要将关键代码放入tryCER 构造的块中。如文档所述,只有catchfinallyfault(不在 C# 中)块被正式定义为 CER 的范围。

于 2013-08-29T19:06:54.117 回答
1

您的意外行为是由于您的代码具有最大的可靠性这一事实。

定义以下方法:

private static bool SwitchToggle(bool toggle) => !toggle;

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

并使用它们代替您的 while 循环体。您会注意到,当调用 SwitchToggle 时,循环变得可中止,而当调用 SafeSwitchToggle 时,它​​不再可中止。

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

于 2020-05-29T10:50:20.730 回答