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

时间:2013-08-29 03:06:44

标签: c# .net multithreading threadabortexception cer

我今晚正在玩Constrained Execution Regions以更好地理解我对细节的理解。我之前曾经使用过它们,但在这些情况下,我大多坚持已建立的模式。无论如何,我注意到一些我无法解释的奇特。

请考虑以下代码。注意,我的目标是.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 =真
  2. didfinally =假
  3. 在你猜之前阅读文档。我在下面列出了相关部分。

      

    约束执行区域(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除了try块之外,还会延迟finally块中的正常中止。但是CLR主机中止只会在CER的finally块中延迟?任何人都可以提供更清晰的信息吗?

3 个答案:

答案 0 :(得分: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.
}

因此PrepareConstrainedRegionsThread.Abort阻止tryThread.Interrupt内部try发出的降级,因此它的行为更像PrepareConstrainedRegions。应该很容易理解为什么这会使try内的代码更安全一些。中止被延​​迟,直到达到数据结构更可能处于一致状态的点。当然,这假设开发人员在更新关键数据结构的过程中没有故意(或无意地)将线程置于可警告状态。

所以基本上trycatch内部注入中止时会增加进一步约束的未记录功能。由于未记录此功能,因此开发人员应谨慎避免依赖此假设,方法是不将关键代码放入CER构造的finally块中。正如文档所述,fault,{{1}}和{{1}}(不在C#中)块正式定义为CER的范围。

答案 1 :(得分: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

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

至于出乎意料的行为,我的想法是你没有通过声明约束区域来达到你所承诺的合同,因此结果没有记录,应该被认为是不可预测的。在尝试中延迟线程中止似乎很奇怪,但我认为这是非预期用法的副作用,这只是值得进一步探索对运行时的学术理解(一类易变的知识,由于无法保证行为,未来的更新可能会改变此行为。)

现在,我不确定在非预期的方式使用上述副作用的程度是多少,但是如果我们退出使用力来影响我们的控制体并让事情运行的背景他们通常的方式,我们得到一些保证:

  • 在某些情况下,Thread.ResetAbort可以防止线程中止
  • 可以捕获ThreadAbortExceptions;整个catch块将运行,如果未重置中止,则在退出catch块时将自动重新抛出ThreadAbortException。
  • 当ThreadAbortException解开callstack时,所有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;
}

我没有关于CER的专家,所以如果我误解了,请告诉我。我希望这会有所帮助:)

答案 2 :(得分: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的其他任何方法,情况也是如此。