Можно ли объяснить это неожиданное поведение PrepareConstrainedRegions и Thread.Abort?

Сегодня вечером я играл с 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 = 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 задерживает нормальные прерывания в try блок в дополнение к finally блок. Но прерывание хоста CLR задерживается только в finally блок ССВ? Кто-нибудь может дать больше ясности по этому поводу?

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 может, в некоторых случаях, предотвратить прерывание потока
  • 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, поэтому, пожалуйста, дайте мне знать, если я неправильно понял. Надеюсь, это поможет:)

Я думаю, что у меня, по крайней мере, есть теория относительно того, что происходит. Если 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.Abort в то время как внутри try блок, так что он ведет себя как Thread.Interrupt, Должно быть легко понять, почему это делает код внутри try немного безопаснее. Прерывание откладывается до тех пор, пока не будет достигнута точка, в которой структуры данных с большей вероятностью будут находиться в согласованном состоянии. Конечно, это предполагает, что разработчик не намеренно (или в этом случае непреднамеренно) переводит поток в состояние оповещения в середине обновления критической структуры данных.

Так в основном PrepareConstrainedRegions имеет добавленную недокументированную особенность дальнейшего ограничения, когда прерывания будут введены в то время как внутри try, Поскольку эта функция не задокументирована, разработчикам будет разумно избегать этого предположения, не помещая критический код в try блок конструкции CER. Как задокументировано только catch, finally, а также fault (не в C#) блоки формально определяются как область видимости CER.

Ваше неожиданное поведение связано с тем, что ваш код имеет максимальную надежность.

Определите следующие методы:

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.

Другие вопросы по тегам