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

Сегодня вечером я играл с областями ограниченного выполнения, чтобы лучше понять мое понимание из более мелких деталей. Я использовал их и раньше, но в основном строго придерживался установленных шаблонов. Так или иначе, я заметил нечто странное, что не могу объяснить.

Рассмотрим следующий код. Обратите внимание, что я нацелился на .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. сделал наконец=Истина
  2. didfinally=ложь

Прежде чем гадать, прочтите документацию. Я включаю соответствующие разделы ниже.

Ограниченная область выполнения (CER) является частью механизма создания надежного управляемого кода. CER определяет область, в которой общеязыковая среда выполнения (CLR) не может создавать внеполосные исключения, которые могут препятствовать выполнению кода в этой области в полном объеме. В этом регионе пользовательский код не может выполнять код, который может привести к возникновению внештатных исключений. Метод PrepareConstrainedRegions должен непосредственно предшествовать блоку try и помечать блоки catch, наконец, и блоки сбоя как ограниченные области выполнения. После того, как код помечен как ограниченная область, он должен вызывать только другой код со строгими контрактами надежности, а код не следует выделять или выполнять виртуальные вызовы неподготовленных или ненадежных методов, если код не подготовлен для обработки сбоев. 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 CER? Может ли кто-нибудь дать больше ясности по этому поводу?


person Brian Gideon    schedule 29.08.2013    source источник


Ответы (3)


[Продолжение из комментариев]

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

person TheXenocide    schedule 16.01.2015
comment
Вы делаете несколько хороших замечаний. Я думаю, что большая часть моей путаницы связана с документацией, подразумевающей, что блок try НЕ является частью CER. Так почему же мы должны ожидать, что внеполосные исключения будут задерживаться во время выполнения блока try. Обычно вы видите пустые блоки try сразу после PrepareConstrainedRegions. Однако мне нравится ваше замечание о неопределенном поведении в этом сценарии. Я определенно могу принять это как причину. Я собираюсь пойти дальше и принять ответ. - person Brian Gideon; 17.01.2015
comment
Да, я на самом деле дважды думал, что понял, что происходит, прежде чем понял, что вся ситуация была вне описанного контекста, лол. - person TheXenocide; 19.01.2015

Я думаю, что у меня по крайней мере есть теория относительно того, что происходит. Если цикл 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.

person Community    schedule 29.08.2013
comment
Я чувствую, что раздел «Ограничения» в документации MSDN по CER адекватно объясняет, что обрабатывается, а что нет: msdn.microsoft.com/en-us/library/ms228973.aspx - person TheXenocide; 15.01.2015
comment
@TheXenocide: Если я правильно прочитал, это говорит вам (программисту), что нельзя делать в CER. Я не думаю, что это говорит вам, каково поведение блока try (который технически не является частью CER согласно документации). Я что-то пропустил? - person Brian Gideon; 16.01.2015
comment
Мой ответ стал длинным и сложным, поэтому я просто добавил реальный ответ. - person TheXenocide; 16.01.2015

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

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

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.

person eTomm    schedule 29.05.2020