пятница, 24 июля 2009 г.

Изучение ThreadAbortException с помощью Rotor

Недавно наткнулся на одну замечательную статью Криса Селлза с интересным названием Plumbing the Depths of the ThreadAbortException Using Rotor Здесь я хочу представить перевод этой статьи.

Изучение ThreadAbortException с помощью Rotor, Крис Селлз

Меня недавно спросили как вызов Thread.Abort может приводить к генерации исключения в совершенно другом потоке. Поиск решения оказался весьма непростой задачкой.

Потоки и исключения

Исключения приводят к альтернативному пути выполнения текущего потока. Рассмотрим следующий пример:

static void Foo() {

  throw new Exception("oops!");

  Console.WriteLine("Never going to get here...");

}

 

static void Main(string[] args) {

  try {

    Foo();

    Console.WriteLine("Never going to get here, either...");

  }

  catch( Exception ex ) {

    Console.WriteLine("Exceptions happen: " + ex.Message);

  }

}

 

Генерация исключения методом Foo прервет выполнения оставшихся операторов в методе Foo и продолжит выполнение в блоке catch метода Main. И, хотя путь исполнения изменен, поток продолжает выполняться, т.е. генерация и перехват исключения происходят в одном и том же потоке. Однако, прерывание (aborting) потока приводит к генерации исключения в другом потоке:

static void Foo() {

  try { while( true ) { ... } }

  catch( ThreadAbortException ex ) { ... }

  finally {...}

 

  // Will never get here if thread aborted

}

 

static void Main(string[] args) {

  Thread thread = new Thread(new ThreadStart(Foo));

  thread.Start();

  thread.Abort(); // cause ThreadAbortException to be thrown

}

 

В этом примере метод Foo выполняется в отдельном потоке и когда он прерывается (путем вызова метода Thread.Abort), выполнение прерывается, что приводит к выполнению блока catch. Это удобный способ сообщить рабочему потоку о том, что он больше не нужен, поэтому он должен освободить ресурсы и завершить работу. Фактически, исключение ThreadAbortException является настолько сильным, что после исполнения блока catch и/или finally, ни одна строка кода не может быть выполнена в этом потоке. Но, если поток уже прерван, то будут вызваны блоки catch и finally, давая возможность потоку сказать последнее слово о том, позволяет он себя прерывать или нет.

Детали реализации

Однако, тот факт, что прерванный поток позволяет выполнять некоторые строки кода само по себе является достижением. Win32 API не предоставляют подобной функциональности. Если поток Win32 остановлен и/или прерван из другого потока, то у него не остается никаких защитных средств, даже для очистки ресурсов. .NET предоставляет весьма интересную возможность «поймать» прерывание потока и попытаться что-либо с этим сделать, особенно учитывая, что такую функциональность не предоставляет операционная система. .NET реализует Thread.Abort из другого потока следующим образом:
    1. Приостанавливает прерываемый поток операционной системы. 2. Устанавливает бит AbortRequested .NET потока. 3. Ожидает перехода прерываемого потока в состояние, в котором возможно его прерывание (interruptible state), путем вызова функций Sleep, Join или wait-функций. 4. Добавляет APC-вызов (Asynchronous procedure call) в очередь APC потока (используя Win32 функцию QueueUserAPC) и возобновляет выполнение потока. 5. Когда прерываемый поток переходит в тревожное состояние (alertable wait state), планировщик вызвает обработчик APC, который устанавливает состояние прерываемого потока в AbortInitiated. Поток переходит в тревожное сотояние только при передаче TRUE в качестве значения bAlertable в функцию типа SleepEx. Если этого не происходит, очередь APC не обработает запрос, а это значит, что вызов ThreadAbort не гарантированно приведет к генерации исключения ThreadAbortException в прерываемом потоке. 6. Когда Common Language Runtime (CLR) возвращает управление прерываемому потоку через «обходную» (“trip”) функцию, которая проверяет все возможные состояния потока для специальных действий, включая будет ли поток прерван. 7. Если состояние потока установлено в AbortInitiated, генерируется исключение ThreadAbortEception, которое может быть обработано (или не обработано) прерываемым потоком в блоках catch и/или finally.

Вызов Thread.Abort приводит к установке CLR определенного флага прерываемого потока, затем этот флаг проверяется в определенных контрольных точках жизни потока, после чего генерируется исключение, если этот флаг был выставлен.

Как я это выяснил

Я выяснил как Thread.Abort приводит к генерации исключения из одного потока в другой путем загрузки и тщательного изучения исходного кода Rotor [1]. Хотя потоки являются низкоуровневыми примитивами (в отличие от WinForms или ASP.NET), исходный код Rotor содержит реализацию и позволяет нам понять, как на самом деле работают те или иные вещи (например, что происходит когда прерывается поток, выполняющий блоки catch или finally).

namespace System.Threading {

  public sealed class Thread {

  ...

    public void Abort() { AbortInternal(); }

 

    [MethodImplAttribute(MethodImplOptions.InternalCall)]

    private extern void AbortInternal();

  }

}

 

Хотя метод AbortInternal помечен как внутренний вызов, я знал, что мне нужен С++ класс, реализующий этом метод, который я нашел путем поиска строки AbortInternal в .h и .cpp файлах. То, что я нашел, совсем не отвечало моим ожиданиям, хотя, если взглянуть на это со стороны, я не был удивлен. В clr\src\vm\ecall.cpp я нашел запись соответствия для внутреннего вызова AbortInternal.

static

ECFunc gThreadFuncs[] = {

  ...

  {FCFuncElement("AbortInternal", NULL, (LPVOID)ThreadNative::Abort)},   

  ...

};

 

Это соответствие сообщает CLR адрес функции, которую необходимо вызвать для реализации AbortInternal. С++ класс ThreadNative содержит объявление и определение этого метода в файлах clr\src\vm\threads.h и clr\src\vm\threads.cpp соответственно. После этого я потратил около 30 минут на изучение мтода Abort (и UserAbort), выясняя, кто что вызывает. Я очень рекомендую подобную деятельность всем своим друзьям; это проясняет разум и очищает душу. Кроме того, я попросил «короля потоков», Майка Вудринга (Mike Woodring) посмотреть на мою оценку ситуации. Именно он мне указал на то, что поток должен перейти в тревожное состояние перед обработкой APC запроса. Спасибо, Майк.

Где же мы сейчас?

Генерация исключения из одного потока в другой работает прекрасно, поскольку CLR заботиться обо всем за нас и не требует реализации этой возможности собственными силами (как в случае работы с Win32). Это еще один пример виртуализации платформы на пути к обеспечению сервисов, которые не предоставляются нижележащей операционной системой. И как я это все выяснил? Все это я выяснил с помощью лучшей документации любого программного обеспечения, имеется ввиду, конечно же, исходный код.

Ссылки

Комментарий

Хочется отметить одну неточность, которую, допустил Крис Селлз, описывая алгоритм реализации метода Thread.Abort. Неточность заключается в пункте 3, в котором говорится, что CLR для прерывания потока ожидает его перехода в interruptible state (поток переходит в это состояние путем вызова функций Sleep, Join или wait-функций). На самом деле interruptible state необходим для успешного завершения потока путем вызова метода Thread.Interrupt, в то время как для прерывания потока путем вызова метода Thread.Abort достаточно, чтобы поток выполнял управляемый код.

4 комментария:

  1. Я ошибся в комментарии, в котором приведен перечень условий, при котором может быть прерван поток.
    На самом деле поток может быть прерван в одном из следующих случаях (Joe Duffy):
    We don’t process thread aborts if you’re executing inside a Constrained Execution Region (CER), finally block, catch block, .cctor, or while running inside unmanaged code. If an abort is requested while inside one of these regions, we process it as soon as you exit (assuming you’re not nested inside another).

    ОтветитьУдалить
  2. Сергей, отличный пост.
    Как в общем-то практически все посты в данном блоге.
    Нравится на мой взгляд правильно выбранный уровень описываемых задач - не слишком простой и не слишком сложный (который бы 90% программистов-среднячков не поняли бы). Т.е. читать интересно и полезно.

    Становлюсь постоянным читателем. Продолжай в том же духе!

    PS. Надо прикрутить защиту от спама.

    ОтветитьУдалить
  3. Кстати, комментарий про то в каких случаях код не прерывается по ThreadAbortException - тоже очень полезный.

    ОтветитьУдалить
  4. Мда, спам я иногда пропускаю:) Спасибо за напоминание, как-нить обязательно прикручу от него что-либо.

    И спасибо за отзывы! И вообще, приятно видеть знакомых rsdn-еров у себя на блоге!

    ОтветитьУдалить